@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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +292 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +7 -0
  5. package/dist/commands/auth.d.ts +2 -0
  6. package/dist/commands/auth.js +265 -0
  7. package/dist/commands/config.d.ts +2 -0
  8. package/dist/commands/config.js +182 -0
  9. package/dist/commands/gtt.d.ts +12 -0
  10. package/dist/commands/gtt.js +276 -0
  11. package/dist/commands/market.d.ts +2 -0
  12. package/dist/commands/market.js +389 -0
  13. package/dist/commands/orders.d.ts +2 -0
  14. package/dist/commands/orders.js +679 -0
  15. package/dist/commands/portfolio.d.ts +2 -0
  16. package/dist/commands/portfolio.js +286 -0
  17. package/dist/commands/types.d.ts +16 -0
  18. package/dist/commands/types.js +1 -0
  19. package/dist/commands/watch.d.ts +18 -0
  20. package/dist/commands/watch.js +285 -0
  21. package/dist/context.d.ts +40 -0
  22. package/dist/context.js +126 -0
  23. package/dist/core/api.d.ts +843 -0
  24. package/dist/core/api.js +472 -0
  25. package/dist/core/auth.d.ts +68 -0
  26. package/dist/core/auth.js +220 -0
  27. package/dist/core/client.d.ts +76 -0
  28. package/dist/core/client.js +285 -0
  29. package/dist/core/config.d.ts +114 -0
  30. package/dist/core/config.js +163 -0
  31. package/dist/core/credentials.d.ts +27 -0
  32. package/dist/core/credentials.js +182 -0
  33. package/dist/core/errors.d.ts +83 -0
  34. package/dist/core/errors.js +152 -0
  35. package/dist/core/instruments.d.ts +87 -0
  36. package/dist/core/instruments.js +288 -0
  37. package/dist/core/paths.d.ts +11 -0
  38. package/dist/core/paths.js +56 -0
  39. package/dist/core/ratelimit.d.ts +57 -0
  40. package/dist/core/ratelimit.js +196 -0
  41. package/dist/core/redact.d.ts +48 -0
  42. package/dist/core/redact.js +181 -0
  43. package/dist/core/schemas.d.ts +662 -0
  44. package/dist/core/schemas.js +433 -0
  45. package/dist/core/secretstore.d.ts +11 -0
  46. package/dist/core/secretstore.js +138 -0
  47. package/dist/core/session.d.ts +37 -0
  48. package/dist/core/session.js +102 -0
  49. package/dist/core/ticker.d.ts +131 -0
  50. package/dist/core/ticker.js +367 -0
  51. package/dist/index.d.ts +17 -0
  52. package/dist/index.js +17 -0
  53. package/dist/output/format.d.ts +31 -0
  54. package/dist/output/format.js +162 -0
  55. package/dist/output/io.d.ts +70 -0
  56. package/dist/output/io.js +143 -0
  57. package/dist/output/table.d.ts +28 -0
  58. package/dist/output/table.js +73 -0
  59. package/dist/run.d.ts +15 -0
  60. package/dist/run.js +161 -0
  61. package/dist/safety.d.ts +93 -0
  62. package/dist/safety.js +154 -0
  63. package/package.json +85 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Client-side rate limiting, per endpoint category.
3
+ *
4
+ * Kite's published limits (https://kite.trade/docs/connect/v3/exceptions/):
5
+ *
6
+ * Quote 1 req/sec <- the binding constraint in practice
7
+ * Historical 3 req/sec
8
+ * Order placement 10 req/sec, 400/min, 5000/day
9
+ * Everything else 10 req/sec
10
+ *
11
+ * The quote limit is what bites first: a naive per-symbol loop over a watchlist
12
+ * is ~1000x slower than one batched /quote/ltp call, which accepts 1000
13
+ * instruments. Batching is handled at the client layer; this module only
14
+ * enforces pacing.
15
+ */
16
+ import { ExitCode, KiteCliError } from './errors.js';
17
+ const LIMITS = {
18
+ quote: { perSecond: 1 },
19
+ historical: { perSecond: 3 },
20
+ order: { perSecond: 10 },
21
+ default: { perSecond: 10 },
22
+ };
23
+ /**
24
+ * A token bucket that refills continuously.
25
+ *
26
+ * Deliberately sized at capacity 1 burst beyond the sustained rate: Kite
27
+ * enforces its limits server-side with little tolerance, and a large burst
28
+ * allowance just converts a clean local wait into a 429.
29
+ */
30
+ class TokenBucket {
31
+ tokens;
32
+ lastRefill;
33
+ ratePerMs;
34
+ capacity;
35
+ now;
36
+ queue = Promise.resolve();
37
+ constructor(perSecond, now) {
38
+ this.now = now;
39
+ this.ratePerMs = perSecond / 1000;
40
+ this.capacity = perSecond;
41
+ this.tokens = perSecond;
42
+ this.lastRefill = now();
43
+ }
44
+ refill() {
45
+ const t = this.now();
46
+ const elapsed = t - this.lastRefill;
47
+ if (elapsed > 0) {
48
+ this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.ratePerMs);
49
+ this.lastRefill = t;
50
+ }
51
+ }
52
+ /** Milliseconds until a token is available. */
53
+ delayForToken() {
54
+ this.refill();
55
+ if (this.tokens >= 1)
56
+ return 0;
57
+ return Math.ceil((1 - this.tokens) / this.ratePerMs);
58
+ }
59
+ /**
60
+ * Wait until a token is available, then consume it.
61
+ *
62
+ * Calls are serialised through a promise chain so that N concurrent callers
63
+ * are spaced correctly rather than all observing the same available token.
64
+ */
65
+ async acquire(signal) {
66
+ const run = this.queue.then(async () => {
67
+ for (;;) {
68
+ const wait = this.delayForToken();
69
+ if (wait === 0)
70
+ break;
71
+ await sleep(wait, signal);
72
+ }
73
+ this.tokens -= 1;
74
+ });
75
+ // Keep the chain alive even if this acquisition rejects (e.g. aborted).
76
+ this.queue = run.catch(() => undefined);
77
+ return run;
78
+ }
79
+ }
80
+ function sleep(ms, signal) {
81
+ return new Promise((resolve, reject) => {
82
+ if (signal?.aborted) {
83
+ reject(signal.reason ?? new Error('Aborted'));
84
+ return;
85
+ }
86
+ const timer = setTimeout(() => {
87
+ signal?.removeEventListener('abort', onAbort);
88
+ resolve();
89
+ }, ms);
90
+ const onAbort = () => {
91
+ clearTimeout(timer);
92
+ reject(signal?.reason ?? new Error('Aborted'));
93
+ };
94
+ signal?.addEventListener('abort', onAbort, { once: true });
95
+ });
96
+ }
97
+ export const ORDER_LIMITS = {
98
+ perMinute: 400,
99
+ perDay: 5000,
100
+ /** Kite rejects further modifications after this many on one order. */
101
+ modificationsPerOrder: 25,
102
+ };
103
+ export class RateLimiter {
104
+ buckets = new Map();
105
+ orderCounters;
106
+ now;
107
+ constructor(now = () => Date.now()) {
108
+ this.now = now;
109
+ for (const [category, config] of Object.entries(LIMITS)) {
110
+ this.buckets.set(category, new TokenBucket(config.perSecond, now));
111
+ }
112
+ this.orderCounters = {
113
+ minuteWindowStart: now(),
114
+ minuteCount: 0,
115
+ dayKey: istDayKey(now()),
116
+ dayCount: 0,
117
+ };
118
+ }
119
+ async acquire(category, signal) {
120
+ // Enforce the documented order caps BEFORE consuming a token, so a runaway
121
+ // loop inside a long-running process is stopped locally with a clear error
122
+ // rather than firing an order Kite will reject with a 429 (which still
123
+ // counts against the cap).
124
+ if (category === 'order')
125
+ this.assertOrderCapacity();
126
+ const bucket = this.buckets.get(category) ?? this.buckets.get('default');
127
+ await bucket.acquire(signal);
128
+ if (category === 'order')
129
+ this.countOrder();
130
+ }
131
+ /**
132
+ * Roll the per-minute and per-IST-day order windows forward if they have
133
+ * elapsed. Shared by the pre-flight cap check and the post-acquire counter so
134
+ * the two never disagree about which window an order falls in.
135
+ */
136
+ rollOrderWindows() {
137
+ const t = this.now();
138
+ if (t - this.orderCounters.minuteWindowStart >= 60_000) {
139
+ this.orderCounters.minuteWindowStart = t;
140
+ this.orderCounters.minuteCount = 0;
141
+ }
142
+ const today = istDayKey(t);
143
+ if (today !== this.orderCounters.dayKey) {
144
+ this.orderCounters.dayKey = today;
145
+ this.orderCounters.dayCount = 0;
146
+ }
147
+ }
148
+ /**
149
+ * Refuse to place another order once a documented cap is reached.
150
+ *
151
+ * These counters reset each process run, so they cannot be authoritative
152
+ * across invocations of a short-lived CLI — this is a backstop against a
153
+ * runaway loop inside ONE long-running process (`kite watch`, a scripted
154
+ * batch, a library embedder), not a mirror of Zerodha's server-side
155
+ * accounting.
156
+ */
157
+ assertOrderCapacity() {
158
+ this.rollOrderWindows();
159
+ if (this.orderCounters.dayCount >= ORDER_LIMITS.perDay) {
160
+ throw new KiteCliError(`This process has placed ${ORDER_LIMITS.perDay} orders — Kite's documented daily cap. It resets at the next IST trading day.`, ExitCode.RateLimit);
161
+ }
162
+ if (this.orderCounters.minuteCount >= ORDER_LIMITS.perMinute) {
163
+ throw new KiteCliError(`This process has placed ${ORDER_LIMITS.perMinute} orders in the last minute — Kite's documented per-minute cap. Slow down and retry shortly.`, ExitCode.RateLimit);
164
+ }
165
+ }
166
+ /**
167
+ * Track the 400/min and 5000/day order caps (see {@link assertOrderCapacity},
168
+ * which enforces them before the order is sent).
169
+ */
170
+ countOrder() {
171
+ this.rollOrderWindows();
172
+ this.orderCounters.minuteCount += 1;
173
+ this.orderCounters.dayCount += 1;
174
+ }
175
+ /** Orders placed by this process in the current minute / IST day. */
176
+ orderUsage() {
177
+ return {
178
+ minute: this.orderCounters.minuteCount,
179
+ day: this.orderCounters.dayCount,
180
+ };
181
+ }
182
+ /** True if this process is approaching a documented order cap. */
183
+ nearOrderLimit() {
184
+ return (this.orderCounters.minuteCount >= ORDER_LIMITS.perMinute * 0.9 ||
185
+ this.orderCounters.dayCount >= ORDER_LIMITS.perDay * 0.9);
186
+ }
187
+ }
188
+ /** IST calendar day key (YYYY-MM-DD). Kite's day boundaries are IST, not UTC. */
189
+ function istDayKey(epochMs) {
190
+ return new Intl.DateTimeFormat('en-CA', {
191
+ timeZone: 'Asia/Kolkata',
192
+ year: 'numeric',
193
+ month: '2-digit',
194
+ day: '2-digit',
195
+ }).format(new Date(epochMs));
196
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Secret redaction for anything that can reach a log, a terminal, or a crash
3
+ * report.
4
+ *
5
+ * The two highest-risk leak paths in this codebase, per the Kite API surface:
6
+ *
7
+ * 1. The `Authorization` header — it is literally `token <api_key>:<access_token>`
8
+ * and is attached to every single request.
9
+ * 2. The WebSocket URL — `access_token` is a *query parameter*, so it appears
10
+ * verbatim in `ws` error messages and stack traces.
11
+ *
12
+ * Everything printed in verbose/debug mode must pass through `redact()`.
13
+ * `test/redact.test.ts` asserts that a known token never survives.
14
+ */
15
+ /**
16
+ * Register a literal secret value so it is scrubbed wherever it appears, even
17
+ * in contexts the pattern rules below do not anticipate (a Kite error message
18
+ * that echoes input, a third-party stack trace).
19
+ *
20
+ * Short values are ignored: scrubbing a 4-character string would mangle
21
+ * unrelated output for no security benefit.
22
+ */
23
+ export declare function registerSecret(value: string | undefined | null): void;
24
+ /** Test-only: drop all registered secrets. */
25
+ export declare function clearRegisteredSecrets(): void;
26
+ export declare const REDACTED = "[redacted]";
27
+ /** Redact secrets from a string. Safe to call on arbitrary text. */
28
+ export declare function redactString(input: string): string;
29
+ /**
30
+ * Deeply redact a value of any shape. Objects are walked by key (secret keys
31
+ * have their values replaced wholesale) and strings are pattern-scrubbed.
32
+ *
33
+ * Cycles are handled; depth is capped to keep this cheap on the hot path.
34
+ */
35
+ export declare function redact(value: unknown, depth?: number, seen?: WeakSet<object>): unknown;
36
+ /**
37
+ * Redact credentials from a URL's query string.
38
+ *
39
+ * This exists specifically for the ticker: `wss://ws.kite.trade?api_key=..&access_token=..`
40
+ * must never be printed or embedded in an error. `api_key` is kept (it is
41
+ * semi-public and useful for debugging); `access_token` is not.
42
+ */
43
+ export declare function redactUrl(url: string | URL): string;
44
+ /**
45
+ * Mask a secret for display, showing only enough to identify which credential
46
+ * it is. Used by `kite config show` and login confirmations.
47
+ */
48
+ export declare function maskSecret(value: string, visible?: number): string;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Secret redaction for anything that can reach a log, a terminal, or a crash
3
+ * report.
4
+ *
5
+ * The two highest-risk leak paths in this codebase, per the Kite API surface:
6
+ *
7
+ * 1. The `Authorization` header — it is literally `token <api_key>:<access_token>`
8
+ * and is attached to every single request.
9
+ * 2. The WebSocket URL — `access_token` is a *query parameter*, so it appears
10
+ * verbatim in `ws` error messages and stack traces.
11
+ *
12
+ * Everything printed in verbose/debug mode must pass through `redact()`.
13
+ * `test/redact.test.ts` asserts that a known token never survives.
14
+ */
15
+ /** Values registered at runtime for exact-match scrubbing. */
16
+ const registeredSecrets = new Set();
17
+ /**
18
+ * Register a literal secret value so it is scrubbed wherever it appears, even
19
+ * in contexts the pattern rules below do not anticipate (a Kite error message
20
+ * that echoes input, a third-party stack trace).
21
+ *
22
+ * Short values are ignored: scrubbing a 4-character string would mangle
23
+ * unrelated output for no security benefit.
24
+ */
25
+ export function registerSecret(value) {
26
+ if (typeof value === 'string' && value.length >= 8) {
27
+ registeredSecrets.add(value);
28
+ }
29
+ }
30
+ /** Test-only: drop all registered secrets. */
31
+ export function clearRegisteredSecrets() {
32
+ registeredSecrets.clear();
33
+ }
34
+ export const REDACTED = '[redacted]';
35
+ /** Key names whose values are always secret, matched case-insensitively. */
36
+ const SECRET_KEYS = new Set([
37
+ 'authorization',
38
+ 'api_secret',
39
+ 'apisecret',
40
+ 'access_token',
41
+ 'accesstoken',
42
+ 'request_token',
43
+ 'requesttoken',
44
+ 'refresh_token',
45
+ 'refreshtoken',
46
+ 'public_token',
47
+ 'publictoken',
48
+ 'enctoken',
49
+ 'checksum',
50
+ 'password',
51
+ 'totp',
52
+ 'x-kite-session',
53
+ ]);
54
+ const PATTERNS = [
55
+ // Authorization: token api_key:access_token (header form, any casing)
56
+ [/\b(authorization\s*:\s*)(?:token\s+)?\S+/gi, `$1${REDACTED}`],
57
+ // token <key>:<secret> appearing bare, e.g. inside a serialized headers object
58
+ [/\btoken\s+[A-Za-z0-9_-]{4,}:[A-Za-z0-9_-]{6,}/g, `token ${REDACTED}`],
59
+ // Any secret-ish key in a query string or form body: access_token=xxxx
60
+ [
61
+ /\b(api_secret|access_token|request_token|refresh_token|public_token|enctoken|checksum|password|totp)=[^&\s"']+/gi,
62
+ `$1=${REDACTED}`,
63
+ ],
64
+ // JSON form: "access_token": "xxxx"
65
+ [
66
+ /("(?:api_secret|access_token|request_token|refresh_token|public_token|enctoken|checksum|password|totp)"\s*:\s*)"[^"]*"/gi,
67
+ `$1"${REDACTED}"`,
68
+ ],
69
+ ];
70
+ /** Redact secrets from a string. Safe to call on arbitrary text. */
71
+ export function redactString(input) {
72
+ let out = input;
73
+ for (const [pattern, replacement] of PATTERNS) {
74
+ out = out.replace(pattern, replacement);
75
+ }
76
+ // Exact-match pass last, so registered values are caught even where no
77
+ // pattern applied.
78
+ for (const secret of registeredSecrets) {
79
+ if (secret && out.includes(secret)) {
80
+ out = out.split(secret).join(REDACTED);
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * Deeply redact a value of any shape. Objects are walked by key (secret keys
87
+ * have their values replaced wholesale) and strings are pattern-scrubbed.
88
+ *
89
+ * Cycles are handled; depth is capped to keep this cheap on the hot path.
90
+ */
91
+ export function redact(value, depth = 0, seen = new WeakSet()) {
92
+ if (depth > 12)
93
+ return '[truncated]';
94
+ if (typeof value === 'string')
95
+ return redactString(value);
96
+ if (value === null || value === undefined)
97
+ return value;
98
+ if (typeof value !== 'object')
99
+ return value;
100
+ if (seen.has(value))
101
+ return '[circular]';
102
+ seen.add(value);
103
+ if (Array.isArray(value)) {
104
+ return value.map((item) => redact(item, depth + 1, seen));
105
+ }
106
+ if (value instanceof Error) {
107
+ return {
108
+ name: value.name,
109
+ message: redactString(value.message),
110
+ stack: value.stack ? redactString(value.stack) : undefined,
111
+ };
112
+ }
113
+ if (value instanceof Headers) {
114
+ const out = {};
115
+ value.forEach((v, k) => {
116
+ out[k] = SECRET_KEYS.has(k.toLowerCase()) ? REDACTED : redactString(v);
117
+ });
118
+ return out;
119
+ }
120
+ if (value instanceof URL) {
121
+ return redactUrl(value);
122
+ }
123
+ // Types with no own enumerable properties must be handled before the generic
124
+ // Object.entries walk below, which would otherwise flatten them to `{}`.
125
+ // Dates matter most: tick timestamps flow straight into `kite watch --json`.
126
+ if (value instanceof Date) {
127
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
128
+ }
129
+ if (value instanceof Map) {
130
+ return redact(Object.fromEntries(value), depth + 1, seen);
131
+ }
132
+ if (value instanceof Set) {
133
+ return redact([...value], depth + 1, seen);
134
+ }
135
+ if (value instanceof RegExp) {
136
+ return value.toString();
137
+ }
138
+ if (Buffer.isBuffer(value)) {
139
+ return `[buffer ${value.length} bytes]`;
140
+ }
141
+ const out = {};
142
+ for (const [key, item] of Object.entries(value)) {
143
+ out[key] = SECRET_KEYS.has(key.toLowerCase()) ? REDACTED : redact(item, depth + 1, seen);
144
+ }
145
+ return out;
146
+ }
147
+ /**
148
+ * Redact credentials from a URL's query string.
149
+ *
150
+ * This exists specifically for the ticker: `wss://ws.kite.trade?api_key=..&access_token=..`
151
+ * must never be printed or embedded in an error. `api_key` is kept (it is
152
+ * semi-public and useful for debugging); `access_token` is not.
153
+ */
154
+ export function redactUrl(url) {
155
+ let parsed;
156
+ try {
157
+ parsed = typeof url === 'string' ? new URL(url) : url;
158
+ }
159
+ catch {
160
+ // Not a parseable URL — fall back to pattern scrubbing.
161
+ return redactString(String(url));
162
+ }
163
+ const copy = new URL(parsed.toString());
164
+ for (const key of [...copy.searchParams.keys()]) {
165
+ if (SECRET_KEYS.has(key.toLowerCase())) {
166
+ copy.searchParams.set(key, REDACTED);
167
+ }
168
+ }
169
+ if (copy.password)
170
+ copy.password = REDACTED;
171
+ return copy.toString();
172
+ }
173
+ /**
174
+ * Mask a secret for display, showing only enough to identify which credential
175
+ * it is. Used by `kite config show` and login confirmations.
176
+ */
177
+ export function maskSecret(value, visible = 4) {
178
+ if (value.length <= visible)
179
+ return '*'.repeat(8);
180
+ return `${'*'.repeat(8)}${value.slice(-visible)}`;
181
+ }