@pungoyal/kite-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +7 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +265 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +182 -0
- package/dist/commands/gtt.d.ts +12 -0
- package/dist/commands/gtt.js +276 -0
- package/dist/commands/market.d.ts +2 -0
- package/dist/commands/market.js +389 -0
- package/dist/commands/orders.d.ts +2 -0
- package/dist/commands/orders.js +679 -0
- package/dist/commands/portfolio.d.ts +2 -0
- package/dist/commands/portfolio.js +286 -0
- package/dist/commands/types.d.ts +16 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/watch.d.ts +18 -0
- package/dist/commands/watch.js +285 -0
- package/dist/context.d.ts +40 -0
- package/dist/context.js +126 -0
- package/dist/core/api.d.ts +843 -0
- package/dist/core/api.js +472 -0
- package/dist/core/auth.d.ts +68 -0
- package/dist/core/auth.js +220 -0
- package/dist/core/client.d.ts +76 -0
- package/dist/core/client.js +285 -0
- package/dist/core/config.d.ts +114 -0
- package/dist/core/config.js +163 -0
- package/dist/core/credentials.d.ts +27 -0
- package/dist/core/credentials.js +182 -0
- package/dist/core/errors.d.ts +83 -0
- package/dist/core/errors.js +152 -0
- package/dist/core/instruments.d.ts +87 -0
- package/dist/core/instruments.js +288 -0
- package/dist/core/paths.d.ts +11 -0
- package/dist/core/paths.js +56 -0
- package/dist/core/ratelimit.d.ts +57 -0
- package/dist/core/ratelimit.js +196 -0
- package/dist/core/redact.d.ts +48 -0
- package/dist/core/redact.js +181 -0
- package/dist/core/schemas.d.ts +662 -0
- package/dist/core/schemas.js +433 -0
- package/dist/core/secretstore.d.ts +11 -0
- package/dist/core/secretstore.js +138 -0
- package/dist/core/session.d.ts +37 -0
- package/dist/core/session.js +102 -0
- package/dist/core/ticker.d.ts +131 -0
- package/dist/core/ticker.js +367 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/output/format.d.ts +31 -0
- package/dist/output/format.js +162 -0
- package/dist/output/io.d.ts +70 -0
- package/dist/output/io.js +143 -0
- package/dist/output/table.d.ts +28 -0
- package/dist/output/table.js +73 -0
- package/dist/run.d.ts +15 -0
- package/dist/run.js +161 -0
- package/dist/safety.d.ts +93 -0
- package/dist/safety.js +154 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Puneet Goyal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# kite-cli
|
|
2
|
+
|
|
3
|
+
[](https://github.com/pungoyal/kite-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@pungoyal/kite-cli)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
An **unofficial**, secure, scriptable command-line interface for the [Zerodha Kite Connect](https://kite.trade/docs/connect/v3/) API.
|
|
9
|
+
|
|
10
|
+
Check your portfolio, stream live quotes, and place orders from the terminal — with credentials in your OS keyring, confirmations on anything that moves money, and clean JSON for piping into `jq`.
|
|
11
|
+
|
|
12
|
+
> **Unofficial, independent project.** Not affiliated with, endorsed by, or sponsored by Zerodha. "Kite" and "Kite Connect" are trademarks of Zerodha Broking Ltd., referenced here only to describe the third-party API this tool works with.
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
$ kite holdings
|
|
16
|
+
╭──────────┬──────┬─────┬──────────┬──────────┬─────────────┬─────────────┬────────┬────────╮
|
|
17
|
+
│ Symbol │ Exch │ Qty │ Avg │ LTP │ Value │ P&L │ P&L % │ Day │
|
|
18
|
+
├──────────┼──────┼─────┼──────────┼──────────┼─────────────┼─────────────┼────────┼────────┤
|
|
19
|
+
│ INFY │ NSE │ 50 │ 1,402.30 │ 1,551.85 │ 77,592.50 │ +₹7,477.50 │ +10.66% │ +0.82% │
|
|
20
|
+
│ TCS │ NSE │ 20 │ 3,890.00 │ 3,802.40 │ 76,048.00 │ -₹1,752.00 │ -2.25% │ -0.41% │
|
|
21
|
+
╰──────────┴──────┴─────┴──────────┴──────────┴─────────────┴─────────────┴────────┴────────╯
|
|
22
|
+
|
|
23
|
+
Invested ₹1,55,915.00
|
|
24
|
+
Current ₹1,53,640.50
|
|
25
|
+
P&L +₹5,725.50 +3.67%
|
|
26
|
+
Day's change +₹287.40
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g @pungoyal/kite-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires **Node 22.12 or newer**.
|
|
36
|
+
|
|
37
|
+
## Getting started
|
|
38
|
+
|
|
39
|
+
You need a [Kite Connect](https://developers.kite.trade) app to get an API key and secret. Set your app's redirect URL to:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
http://127.0.0.1:51101/callback
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
kite login
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This opens your browser, you log in to Zerodha normally (including your TOTP), and the CLI captures the callback on loopback. Your API secret goes into your OS keyring; the daily access token is stored alongside it.
|
|
52
|
+
|
|
53
|
+
**Want to try it without a subscription or real money?** Zerodha runs a public sandbox:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
kite login --env sandbox
|
|
57
|
+
kite --env sandbox holdings
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Portfolio
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
kite holdings # long-term holdings with P&L
|
|
66
|
+
kite positions # open positions
|
|
67
|
+
kite positions --day # intraday only
|
|
68
|
+
kite funds # available margin
|
|
69
|
+
kite convert NSE:INFY --quantity 10 --from MIS --to CNC
|
|
70
|
+
kite authorise # authorise demat holdings for selling
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If a sell order fails with exit code 12 ("needs authorisation at depository"), run `kite authorise` — it requests a CDSL authorisation and opens the browser page that completes it. Pass specific ISINs to authorise only those instruments.
|
|
74
|
+
|
|
75
|
+
### Market data
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
kite quote NSE:INFY NSE:TCS # full quotes
|
|
79
|
+
kite quote NSE:INFY --depth # with the 5-level order book
|
|
80
|
+
kite ltp NSE:INFY # just the last traded price
|
|
81
|
+
kite ohlc NSE:RELIANCE
|
|
82
|
+
|
|
83
|
+
kite history NSE:INFY --from 90d
|
|
84
|
+
kite history NSE:INFY -i 5minute --from 7d --csv > infy.csv
|
|
85
|
+
|
|
86
|
+
kite instruments search "nifty bank"
|
|
87
|
+
kite instruments search INFY --exchange NFO --type CE
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Date arguments accept `YYYY-MM-DD` or relative offsets like `30d`, `6m`, `1y`.
|
|
91
|
+
|
|
92
|
+
Long ranges are chunked automatically to respect Kite's per-interval limits, then merged and de-duplicated.
|
|
93
|
+
|
|
94
|
+
### Live streaming
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
kite watch NSE:INFY NSE:TCS # self-updating table
|
|
98
|
+
kite watch --holdings # stream your whole portfolio
|
|
99
|
+
kite watch --positions --orders # positions plus live order updates
|
|
100
|
+
kite watch NSE:INFY --json | jq # NDJSON for piping
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Trading
|
|
104
|
+
|
|
105
|
+
Every order previews the **resolved** order and asks for confirmation:
|
|
106
|
+
|
|
107
|
+
```console
|
|
108
|
+
$ kite orders place NSE:INFY --side BUY --quantity 10 --type LIMIT --price 1500
|
|
109
|
+
|
|
110
|
+
Place BUY order for 10 INFY
|
|
111
|
+
Instrument NSE:INFY
|
|
112
|
+
Resolved INFOSYS LIMITED (token 408065)
|
|
113
|
+
Side BUY
|
|
114
|
+
Quantity 10
|
|
115
|
+
Order type LIMIT
|
|
116
|
+
Price ₹1,500.00
|
|
117
|
+
Product CNC
|
|
118
|
+
Est. value ₹15,000.00
|
|
119
|
+
Tag kcmrt88o648c1bce
|
|
120
|
+
|
|
121
|
+
◆ Place BUY order for 10 INFY?
|
|
122
|
+
│ ● Yes / ○ No
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
kite orders list # today's orderbook
|
|
127
|
+
kite orders list --open # working orders only
|
|
128
|
+
kite orders get 250720000123456 # full state history and fills
|
|
129
|
+
kite orders modify <id> --price 1520
|
|
130
|
+
kite orders cancel <id>
|
|
131
|
+
kite trades # today's fills
|
|
132
|
+
|
|
133
|
+
kite gtt list
|
|
134
|
+
kite gtt place NSE:INFY --side SELL --quantity 10 \
|
|
135
|
+
--trigger 1400 --price 1395 \
|
|
136
|
+
--trigger 1700 --price 1695 # two-leg OCO
|
|
137
|
+
kite gtt delete <id>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Add `--dry-run` to any of these to see exactly what would be sent, without sending it.
|
|
141
|
+
|
|
142
|
+
### Scripting
|
|
143
|
+
|
|
144
|
+
Every command supports `--json`, writes data to stdout and everything else to stderr, and returns a meaningful exit code.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
kite positions --json | jq '.net[] | select(.pnl < 0) | .tradingsymbol'
|
|
148
|
+
kite holdings --json | jq '[.[] | .pnl] | add'
|
|
149
|
+
|
|
150
|
+
kite whoami --json || kite login # exit code 3 means "no session"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Code | Meaning |
|
|
154
|
+
|-----:|---------|
|
|
155
|
+
| 0 | Success |
|
|
156
|
+
| 1 | Unclassified failure |
|
|
157
|
+
| 2 | Bad usage — unknown flag, missing or invalid argument |
|
|
158
|
+
| 3 | Not logged in, or the session expired |
|
|
159
|
+
| 4 | Kite rejected the input |
|
|
160
|
+
| 5 | Order rejected by the exchange |
|
|
161
|
+
| 6 | Insufficient margin |
|
|
162
|
+
| 7 | Insufficient holdings to sell |
|
|
163
|
+
| 8 | Rate limited |
|
|
164
|
+
| 9 | Kite or its upstream OMS is unreachable |
|
|
165
|
+
| 10 | You declined a confirmation |
|
|
166
|
+
| 11 | Confirmation required but the terminal is not interactive |
|
|
167
|
+
| 12 | Holdings need depository authorisation — run `kite authorise` |
|
|
168
|
+
| 13 | Blocked by the local kill switch or order value cap |
|
|
169
|
+
|
|
170
|
+
`NO_COLOR` is honoured, and colour is disabled automatically when stdout is not a TTY.
|
|
171
|
+
|
|
172
|
+
## Safety
|
|
173
|
+
|
|
174
|
+
This tool spends real money, so the defaults are conservative.
|
|
175
|
+
|
|
176
|
+
**Confirmation.** Order commands render the resolved order — the actual instrument token, lot size, and computed value, not an echo of your flags — and wait for confirmation. That's deliberate: a flag echo can't catch "I typed the wrong symbol and it resolved to a different contract," which is the expensive mistake.
|
|
177
|
+
|
|
178
|
+
**Escalation.** Above ₹1,00,000 (configurable) a keystroke isn't enough; you have to type the trading symbol.
|
|
179
|
+
|
|
180
|
+
**Non-interactive means refuse.** With no TTY and no `--yes`, order commands exit non-zero rather than silently proceeding.
|
|
181
|
+
|
|
182
|
+
**`--yes` is call-site only.** There is deliberately no config setting to disable confirmations globally. Bypassing safety has to be an explicit act every time.
|
|
183
|
+
|
|
184
|
+
**Kill switch and cap.**
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
kite config set trading.enabled false # refuse all order commands
|
|
188
|
+
kite config set trading.maxOrderValue 50000 # refuse orders above ₹50,000
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**No blind retries.** Kite has no idempotency key, so a timed-out order placement is genuinely ambiguous — it may have executed. This CLI never retries a write. Instead it tags every order with a unique value, and on failure queries the orderbook for that tag to tell you what actually happened:
|
|
192
|
+
|
|
193
|
+
```console
|
|
194
|
+
$ kite orders place NSE:INFY -s BUY -q 10 --type LIMIT --price 1500 --yes
|
|
195
|
+
! The order request failed: Request timed out
|
|
196
|
+
· Checking whether it reached Kite anyway…
|
|
197
|
+
! The order DID reach Kite: 250720000123456 (COMPLETE).
|
|
198
|
+
· It was not placed twice. Do not re-run this command.
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Automatic retries are restricted to `GET`/`HEAD` at the transport layer. `POST`, `PUT` and `DELETE` are never retried — in this API those are place, modify and cancel.
|
|
202
|
+
|
|
203
|
+
## Security
|
|
204
|
+
|
|
205
|
+
**Credentials** are resolved in this order:
|
|
206
|
+
|
|
207
|
+
1. `KITE_API_SECRET` / `KITE_ACCESS_TOKEN` environment variables — for CI and containers, never persisted
|
|
208
|
+
2. OS keyring — macOS Keychain, Windows Credential Manager, Linux Secret Service
|
|
209
|
+
3. Encrypted file at `~/.config/kite/credentials.enc` — scrypt (N=2¹⁷) + AES-256-GCM, mode `0600`, with the KDF header bound as authenticated data so parameters can't be downgraded
|
|
210
|
+
|
|
211
|
+
Your API secret is never accepted as a command-line argument, because argv is visible to any local process via `ps` and lands in shell history. It's prompted for, or read from the environment.
|
|
212
|
+
|
|
213
|
+
**Redaction.** Access tokens are registered with a scrubber that runs over every log line, error message, and stack trace. The two paths that carry a token — the `Authorization` header and the WebSocket URL, where it's a query parameter — are covered explicitly and [tested](test/redact.test.ts).
|
|
214
|
+
|
|
215
|
+
**TOTP.** This CLI will never ask for or store your 2FA seed. Storing it next to your API secret would collapse both authentication factors into one, which is exactly what the SEBI 2FA mandate exists to prevent. Login happens in your browser; the CLI only sees the resulting request token.
|
|
216
|
+
|
|
217
|
+
**Supply chain.** 10 direct runtime dependencies, most of them zero-dependency. Published from GitHub Actions via [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) with OIDC — no long-lived publish token exists. Provenance attestation is generated automatically; verify it with `npm audit signatures`. All CI actions are pinned to full commit SHAs, and dependency lifecycle scripts are disabled.
|
|
218
|
+
|
|
219
|
+
To report a vulnerability, see [SECURITY.md](SECURITY.md).
|
|
220
|
+
|
|
221
|
+
## Configuration
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
kite config show
|
|
225
|
+
kite config set <key> <value>
|
|
226
|
+
kite config path
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
| Key | Description |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `trading.enabled` | Master kill switch for all order commands |
|
|
232
|
+
| `trading.confirm` | Require confirmation before money-moving actions |
|
|
233
|
+
| `trading.maxOrderValue` | Refuse any single order above this rupee value |
|
|
234
|
+
| `trading.strictConfirmAbove` | Above this value, require typing the symbol |
|
|
235
|
+
| `output.color` | `auto`, `always`, or `never` |
|
|
236
|
+
| `output.compact` | Render tables without borders |
|
|
237
|
+
| `redirectPort` / `redirectPath` | Loopback callback URL for login |
|
|
238
|
+
|
|
239
|
+
Config lives at `~/.config/kite/config.json` (`0600`). Override the location with `KITE_CONFIG_DIR`.
|
|
240
|
+
|
|
241
|
+
## Things worth knowing about Kite
|
|
242
|
+
|
|
243
|
+
- **Sessions expire at 6:00 AM IST daily.** This is a regulatory requirement and there's no way around it — you log in once per trading day. Refresh tokens exist in the API but are only issued to exchange-approved platforms, not individual subscribers.
|
|
244
|
+
- **Logging into Kite web invalidates your API session.** The CLI can't detect this until a request comes back 403.
|
|
245
|
+
- **Order acceptance is not execution.** A returned order ID means the OMS accepted the request. Check `kite orders get <id>` for what actually happened.
|
|
246
|
+
- **Rate limits are tight**: quotes 1/sec, historical 3/sec, orders 10/sec (plus 400/min and 5,000/day). The CLI paces requests for you and batches quotes automatically — one call handles up to 1,000 instruments.
|
|
247
|
+
- **Kite caps order modifications at 25** per order. After that you must cancel and re-place.
|
|
248
|
+
- **Instruments are cached by `exchange:tradingsymbol`, never by token.** Exchanges reuse numeric instrument tokens after expiry, so a token-keyed cache silently resolves to the wrong contract after a rollover.
|
|
249
|
+
- **Mutual funds are read-only** over the API — placing MF orders requires a bank debit that has no API path.
|
|
250
|
+
|
|
251
|
+
## Library use
|
|
252
|
+
|
|
253
|
+
The client is exported if you want it without the CLI:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { KiteClient, KiteApi, endpointsFor } from '@pungoyal/kite-cli';
|
|
257
|
+
|
|
258
|
+
const client = new KiteClient({
|
|
259
|
+
apiKey: process.env.KITE_API_KEY!,
|
|
260
|
+
accessToken: process.env.KITE_ACCESS_TOKEN!,
|
|
261
|
+
endpoints: endpointsFor('production'),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const api = new KiteApi(client);
|
|
265
|
+
console.log(await api.getHoldings());
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
You get the same rate limiting, response validation, redaction, and error taxonomy the CLI uses.
|
|
269
|
+
|
|
270
|
+
## Development
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
npm install
|
|
274
|
+
npm run dev -- holdings # run from source
|
|
275
|
+
npm test
|
|
276
|
+
npm run typecheck
|
|
277
|
+
npm run build
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Contributing
|
|
281
|
+
|
|
282
|
+
Bug reports, ideas, and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and the two non-negotiables (safety defaults and secret redaction), and [CHANGELOG.md](CHANGELOG.md) for release history. Security issues go through [SECURITY.md](SECURITY.md), never a public issue.
|
|
283
|
+
|
|
284
|
+
## Disclaimer
|
|
285
|
+
|
|
286
|
+
This is an unofficial, independent, community project. It is **not affiliated with, endorsed by, or sponsored by Zerodha**. "Kite", "Kite Connect", and "Zerodha" are trademarks of Zerodha Broking Ltd.; this project references them only to identify the third-party API it interoperates with (nominative use) and claims no rights to those marks.
|
|
287
|
+
|
|
288
|
+
Trading involves risk of financial loss. This software is provided as-is under the MIT licence — you are responsible for every order it places on your behalf. Test with `--env sandbox` and `--dry-run` before trusting it with real money.
|
|
289
|
+
|
|
290
|
+
## Licence
|
|
291
|
+
|
|
292
|
+
[MIT](LICENSE)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import module from 'node:module';
|
|
3
|
+
// Node's on-disk compile cache. Cheap to enable and a measurable win on
|
|
4
|
+
// startup, which matters for a CLI people run in loops and shell prompts.
|
|
5
|
+
module.enableCompileCache?.();
|
|
6
|
+
const { run } = await import('./run.js');
|
|
7
|
+
process.exitCode = await run();
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { isCancel, note, password, text } from '@clack/prompts';
|
|
2
|
+
import { buildLoginUrl, computeChecksum, generateState, openBrowser, redirectUrlFor, waitForCallback, } from '../core/auth.js';
|
|
3
|
+
import { SANDBOX_CREDENTIALS, saveConfig } from '../core/config.js';
|
|
4
|
+
import { deleteAllSecrets, getSecret, keyringAvailable, setSecret } from '../core/credentials.js';
|
|
5
|
+
import { AbortedError, ExitCode, KiteCliError } from '../core/errors.js';
|
|
6
|
+
import { maskSecret, registerSecret } from '../core/redact.js';
|
|
7
|
+
import { clearSessionMeta, isExpired, loadSessionMeta, nextTokenExpiry, saveSessionMeta, timeUntilExpiry, } from '../core/session.js';
|
|
8
|
+
import { dateTime } from '../output/format.js';
|
|
9
|
+
import { renderKeyValue } from '../output/table.js';
|
|
10
|
+
export const authCommands = (program, run) => {
|
|
11
|
+
program
|
|
12
|
+
.command('login')
|
|
13
|
+
.description('Authenticate with Kite and store a session')
|
|
14
|
+
.option('--manual', 'Paste the request token by hand instead of using a local callback server')
|
|
15
|
+
.option('--api-key <key>', 'Kite Connect API key (prompted for if absent)')
|
|
16
|
+
.option('--force', 'Log in again even if the current session is still valid')
|
|
17
|
+
.action(run(login));
|
|
18
|
+
program
|
|
19
|
+
.command('logout')
|
|
20
|
+
.description('Invalidate the session and remove the stored access token')
|
|
21
|
+
.option('--all', 'Also remove the stored API secret')
|
|
22
|
+
.action(run(logout));
|
|
23
|
+
program.command('whoami').description('Show the current session and account details').action(run(whoami));
|
|
24
|
+
};
|
|
25
|
+
async function login(ctx, opts) {
|
|
26
|
+
const { io } = ctx;
|
|
27
|
+
if (!opts.force && ctx.client.hasSession() && ctx.session && !isExpired(ctx.session)) {
|
|
28
|
+
io.success(`Already logged in as ${io.bold(ctx.session.userName ?? ctx.session.userId)} ` +
|
|
29
|
+
`(expires in ${timeUntilExpiry(ctx.session)}).`);
|
|
30
|
+
io.info('Use --force to log in again.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const isSandbox = ctx.env === 'sandbox';
|
|
34
|
+
// --- credentials -------------------------------------------------------
|
|
35
|
+
let apiKey = opts.apiKey ?? (isSandbox ? SANDBOX_CREDENTIALS.apiKey : (ctx.config.apiKey ?? ''));
|
|
36
|
+
let apiSecret;
|
|
37
|
+
if (isSandbox) {
|
|
38
|
+
apiKey = SANDBOX_CREDENTIALS.apiKey;
|
|
39
|
+
apiSecret = SANDBOX_CREDENTIALS.apiSecret;
|
|
40
|
+
io.info('Using the public sandbox credentials. No real money is involved.');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (!apiKey) {
|
|
44
|
+
apiKey = await promptText('Kite Connect API key', 'Create an app at https://developers.kite.trade to get one.');
|
|
45
|
+
}
|
|
46
|
+
const stored = await getSecret('api_secret', { env: ctx.env });
|
|
47
|
+
if (stored && !opts.force) {
|
|
48
|
+
apiSecret = stored.value;
|
|
49
|
+
io.info(`Using stored API secret (${maskSecret(apiSecret)}) from the ${stored.backend}.`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Prompted, never accepted as a CLI argument: argv is visible to any
|
|
53
|
+
// local process via `ps`, and lands in shell history.
|
|
54
|
+
apiSecret = await promptSecret('Kite Connect API secret');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
registerSecret(apiSecret);
|
|
58
|
+
ctx.client.setAccessToken(undefined);
|
|
59
|
+
// --- obtain a request token --------------------------------------------
|
|
60
|
+
const state = generateState();
|
|
61
|
+
const loginUrl = buildLoginUrl({ apiKey, endpoints: ctx.endpoints, state });
|
|
62
|
+
let requestToken;
|
|
63
|
+
if (opts.manual) {
|
|
64
|
+
requestToken = await manualFlow(ctx, loginUrl);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
requestToken = await callbackFlow(ctx, loginUrl);
|
|
68
|
+
}
|
|
69
|
+
// --- exchange for an access token --------------------------------------
|
|
70
|
+
const checksum = computeChecksum(apiKey, requestToken, apiSecret);
|
|
71
|
+
// Rebuild the client with the right API key, in case this is a first login.
|
|
72
|
+
const { KiteClient } = await import('../core/client.js');
|
|
73
|
+
const { KiteApi } = await import('../core/api.js');
|
|
74
|
+
const client = new KiteClient({
|
|
75
|
+
apiKey,
|
|
76
|
+
endpoints: ctx.endpoints,
|
|
77
|
+
limiter: ctx.client.limiter,
|
|
78
|
+
debug: ctx.options.debug ?? false,
|
|
79
|
+
onDebug: (message) => io.note(io.dim(message)),
|
|
80
|
+
});
|
|
81
|
+
const api = new KiteApi(client);
|
|
82
|
+
const session = await api.createSession({ requestToken, checksum });
|
|
83
|
+
registerSecret(session.access_token);
|
|
84
|
+
// --- persist ------------------------------------------------------------
|
|
85
|
+
const backend = await setSecret('access_token', session.access_token, {
|
|
86
|
+
env: ctx.env,
|
|
87
|
+
});
|
|
88
|
+
if (!isSandbox) {
|
|
89
|
+
await setSecret('api_secret', apiSecret, { env: ctx.env });
|
|
90
|
+
}
|
|
91
|
+
const expiresAt = nextTokenExpiry();
|
|
92
|
+
await saveSessionMeta({
|
|
93
|
+
userId: session.user_id,
|
|
94
|
+
userName: session.user_name,
|
|
95
|
+
broker: session.broker,
|
|
96
|
+
env: ctx.env,
|
|
97
|
+
apiKey,
|
|
98
|
+
expiresAt: expiresAt.toISOString(),
|
|
99
|
+
loginTime: session.login_time,
|
|
100
|
+
exchanges: session.exchanges,
|
|
101
|
+
products: session.products,
|
|
102
|
+
});
|
|
103
|
+
if (ctx.config.apiKey !== apiKey && !isSandbox) {
|
|
104
|
+
await saveConfig({ ...ctx.config, apiKey });
|
|
105
|
+
}
|
|
106
|
+
if (io.json) {
|
|
107
|
+
io.writeJson({
|
|
108
|
+
user_id: session.user_id,
|
|
109
|
+
user_name: session.user_name,
|
|
110
|
+
env: ctx.env,
|
|
111
|
+
expires_at: expiresAt.toISOString(),
|
|
112
|
+
storage: backend,
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
io.success(`Logged in as ${io.bold(session.user_name ?? session.user_id)}.`);
|
|
117
|
+
io.info(`Session expires ${dateTime(expiresAt)} IST (Kite invalidates all tokens at 6 AM daily).`);
|
|
118
|
+
io.info(backend === 'keyring'
|
|
119
|
+
? 'Credentials stored in your OS keyring.'
|
|
120
|
+
: `Credentials stored in an encrypted file (no OS keyring available).`);
|
|
121
|
+
}
|
|
122
|
+
/** Browser + loopback callback. The default, and the least error-prone path. */
|
|
123
|
+
async function callbackFlow(ctx, loginUrl) {
|
|
124
|
+
const { io } = ctx;
|
|
125
|
+
const port = ctx.config.redirectPort;
|
|
126
|
+
const path = ctx.config.redirectPath;
|
|
127
|
+
const redirectUrl = redirectUrlFor(port, path);
|
|
128
|
+
const server = waitForCallback({ port, path, state: extractState(loginUrl) });
|
|
129
|
+
io.note('');
|
|
130
|
+
io.info(`Your Kite app's redirect URL must be exactly: ${io.bold(redirectUrl)}`);
|
|
131
|
+
io.info('Set it at https://developers.kite.trade if login fails.');
|
|
132
|
+
io.note('');
|
|
133
|
+
const opened = await openBrowser(loginUrl);
|
|
134
|
+
if (opened) {
|
|
135
|
+
io.info('Opened your browser to complete login…');
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
io.warn('Could not open a browser automatically. Open this URL manually:');
|
|
139
|
+
io.note(` ${loginUrl}`);
|
|
140
|
+
}
|
|
141
|
+
io.info('Waiting for the callback (Ctrl-C to abort)…');
|
|
142
|
+
try {
|
|
143
|
+
const result = await server.promise;
|
|
144
|
+
return result.requestToken;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
server.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Fallback for remote shells, where no browser can reach 127.0.0.1. */
|
|
151
|
+
async function manualFlow(ctx, loginUrl) {
|
|
152
|
+
const { io } = ctx;
|
|
153
|
+
io.note('');
|
|
154
|
+
io.info('Open this URL, log in, then copy the `request_token` from the redirect URL:');
|
|
155
|
+
io.note(` ${loginUrl}`);
|
|
156
|
+
io.note('');
|
|
157
|
+
const token = await promptText('request_token');
|
|
158
|
+
if (!token)
|
|
159
|
+
throw new AbortedError();
|
|
160
|
+
registerSecret(token);
|
|
161
|
+
return token;
|
|
162
|
+
}
|
|
163
|
+
function extractState(loginUrl) {
|
|
164
|
+
const params = new URL(loginUrl).searchParams.get('redirect_params') ?? '';
|
|
165
|
+
return new URLSearchParams(params).get('state') ?? '';
|
|
166
|
+
}
|
|
167
|
+
async function logout(ctx, opts) {
|
|
168
|
+
const { io } = ctx;
|
|
169
|
+
// Best-effort server-side invalidation; a failure here must not stop us
|
|
170
|
+
// clearing local state, or the user is stuck with credentials they cannot
|
|
171
|
+
// remove.
|
|
172
|
+
if (ctx.client.hasSession()) {
|
|
173
|
+
const stored = await getSecret('access_token', { env: ctx.env });
|
|
174
|
+
if (stored) {
|
|
175
|
+
try {
|
|
176
|
+
await ctx.api.invalidateSession(stored.value);
|
|
177
|
+
io.info('Session invalidated with Kite.');
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
io.warn('Could not reach Kite to invalidate the session; clearing local credentials anyway.');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const { deleteSecret } = await import('../core/credentials.js');
|
|
185
|
+
if (opts.all) {
|
|
186
|
+
await deleteAllSecrets({ env: ctx.env });
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
await deleteSecret('access_token', { env: ctx.env });
|
|
190
|
+
}
|
|
191
|
+
await clearSessionMeta();
|
|
192
|
+
if (io.json) {
|
|
193
|
+
io.writeJson({ logged_out: true, removed_api_secret: Boolean(opts.all) });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
io.success(opts.all ? 'Logged out and removed the stored API secret.' : 'Logged out.');
|
|
197
|
+
if (!opts.all)
|
|
198
|
+
io.info('Your API secret is still stored. Use `kite logout --all` to remove it.');
|
|
199
|
+
}
|
|
200
|
+
async function whoami(ctx) {
|
|
201
|
+
const { io } = ctx;
|
|
202
|
+
const meta = await loadSessionMeta();
|
|
203
|
+
if (!ctx.client.hasSession()) {
|
|
204
|
+
// Exit non-zero in both modes: a script checking `kite whoami` must be able
|
|
205
|
+
// to branch on the exit code, not parse stdout.
|
|
206
|
+
process.exitCode = ExitCode.Auth;
|
|
207
|
+
if (io.json) {
|
|
208
|
+
io.writeJson({ logged_in: false });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
io.error('Not logged in.');
|
|
212
|
+
io.info('Run `kite login` to start a session.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Hit the API so this reflects reality, not just what we cached.
|
|
216
|
+
const profile = await ctx.api.getProfile(ctx.signal);
|
|
217
|
+
if (io.json) {
|
|
218
|
+
io.writeJson({
|
|
219
|
+
logged_in: true,
|
|
220
|
+
env: ctx.env,
|
|
221
|
+
expires_at: meta?.expiresAt,
|
|
222
|
+
profile,
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
io.line(renderKeyValue(io, [
|
|
227
|
+
['User', `${profile.user_name ?? '—'} (${profile.user_id})`],
|
|
228
|
+
['Email', profile.email ?? '—'],
|
|
229
|
+
['Broker', profile.broker ?? '—'],
|
|
230
|
+
['Environment', ctx.env === 'sandbox' ? io.cyan('sandbox') : 'production'],
|
|
231
|
+
['Exchanges', profile.exchanges.join(', ') || '—'],
|
|
232
|
+
['Products', profile.products.join(', ') || '—'],
|
|
233
|
+
[
|
|
234
|
+
'Session expires',
|
|
235
|
+
meta ? `${dateTime(meta.expiresAt)} IST (${timeUntilExpiry(meta)} left)` : 'unknown (token from environment)',
|
|
236
|
+
],
|
|
237
|
+
['Keyring', (await keyringAvailable()) ? 'available' : 'unavailable (using encrypted file)'],
|
|
238
|
+
]));
|
|
239
|
+
}
|
|
240
|
+
async function promptText(message, hint) {
|
|
241
|
+
if (!process.stdin.isTTY) {
|
|
242
|
+
throw new KiteCliError(`${message} is required but this is not an interactive terminal.`, ExitCode.ConfirmationRequired, 'Supply it via a flag or environment variable.');
|
|
243
|
+
}
|
|
244
|
+
if (hint)
|
|
245
|
+
note(hint);
|
|
246
|
+
const answer = await text({
|
|
247
|
+
message,
|
|
248
|
+
validate: (value) => ((value ?? '').trim() === '' ? 'Required.' : undefined),
|
|
249
|
+
});
|
|
250
|
+
if (isCancel(answer))
|
|
251
|
+
throw new AbortedError();
|
|
252
|
+
return String(answer).trim();
|
|
253
|
+
}
|
|
254
|
+
async function promptSecret(message) {
|
|
255
|
+
if (!process.stdin.isTTY) {
|
|
256
|
+
throw new KiteCliError(`${message} is required but this is not an interactive terminal.`, ExitCode.ConfirmationRequired, 'Set KITE_API_SECRET instead.');
|
|
257
|
+
}
|
|
258
|
+
const answer = await password({
|
|
259
|
+
message,
|
|
260
|
+
validate: (value) => ((value ?? '').trim() === '' ? 'Required.' : undefined),
|
|
261
|
+
});
|
|
262
|
+
if (isCancel(answer))
|
|
263
|
+
throw new AbortedError();
|
|
264
|
+
return String(answer).trim();
|
|
265
|
+
}
|