@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
|
@@ -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
|
+
}
|