@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,433 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Runtime schemas for Kite API responses.
|
|
4
|
+
*
|
|
5
|
+
* Two rules govern everything here:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Every object is loose.** Zerodha adds fields without notice; a strict
|
|
8
|
+
* schema would turn a benign API addition into a broken CLI for every user.
|
|
9
|
+
* We validate the fields we depend on and pass the rest through.
|
|
10
|
+
*
|
|
11
|
+
* 2. **`status` is never an enum.** The order docs list the known states and
|
|
12
|
+
* then say verbatim "There may be other values as well." An exhaustive
|
|
13
|
+
* union would reject real orders. We use `z.string()` plus named constants.
|
|
14
|
+
*
|
|
15
|
+
* Binary WebSocket frames are deliberately NOT validated here — they are
|
|
16
|
+
* fixed-length structs, parsed by hand in ticker.ts. Running 3000 instruments
|
|
17
|
+
* of full-mode ticks through a validator would be both the wrong tool and a
|
|
18
|
+
* real cost on the hot path.
|
|
19
|
+
*/
|
|
20
|
+
/** Kite wraps every successful response as { status: "success", data: ... }. */
|
|
21
|
+
export const EnvelopeSchema = z.looseObject({
|
|
22
|
+
status: z.string(),
|
|
23
|
+
data: z.unknown().optional(),
|
|
24
|
+
message: z.string().optional(),
|
|
25
|
+
error_type: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
export const ErrorEnvelopeSchema = z.looseObject({
|
|
28
|
+
status: z.literal('error'),
|
|
29
|
+
message: z.string().default('Unknown error'),
|
|
30
|
+
error_type: z.string().default('GeneralException'),
|
|
31
|
+
});
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Session / user
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
export const SessionSchema = z.looseObject({
|
|
36
|
+
user_id: z.string(),
|
|
37
|
+
user_name: z.string().optional(),
|
|
38
|
+
user_shortname: z.string().optional(),
|
|
39
|
+
email: z.string().optional(),
|
|
40
|
+
user_type: z.string().optional(),
|
|
41
|
+
broker: z.string().optional(),
|
|
42
|
+
exchanges: z.array(z.string()).default([]),
|
|
43
|
+
products: z.array(z.string()).default([]),
|
|
44
|
+
order_types: z.array(z.string()).default([]),
|
|
45
|
+
access_token: z.string(),
|
|
46
|
+
public_token: z.string().optional(),
|
|
47
|
+
refresh_token: z.string().optional(),
|
|
48
|
+
login_time: z.string().optional(),
|
|
49
|
+
});
|
|
50
|
+
export const ProfileSchema = z.looseObject({
|
|
51
|
+
user_id: z.string(),
|
|
52
|
+
user_name: z.string().optional(),
|
|
53
|
+
user_shortname: z.string().optional(),
|
|
54
|
+
email: z.string().optional(),
|
|
55
|
+
user_type: z.string().optional(),
|
|
56
|
+
broker: z.string().optional(),
|
|
57
|
+
exchanges: z.array(z.string()).default([]),
|
|
58
|
+
products: z.array(z.string()).default([]),
|
|
59
|
+
order_types: z.array(z.string()).default([]),
|
|
60
|
+
});
|
|
61
|
+
const SegmentMarginSchema = z.looseObject({
|
|
62
|
+
enabled: z.boolean().optional(),
|
|
63
|
+
net: z.number().optional(),
|
|
64
|
+
available: z
|
|
65
|
+
.looseObject({
|
|
66
|
+
cash: z.number().optional(),
|
|
67
|
+
opening_balance: z.number().optional(),
|
|
68
|
+
live_balance: z.number().optional(),
|
|
69
|
+
intraday_payin: z.number().optional(),
|
|
70
|
+
adhoc_margin: z.number().optional(),
|
|
71
|
+
collateral: z.number().optional(),
|
|
72
|
+
})
|
|
73
|
+
.optional(),
|
|
74
|
+
utilised: z
|
|
75
|
+
.looseObject({
|
|
76
|
+
debits: z.number().optional(),
|
|
77
|
+
exposure: z.number().optional(),
|
|
78
|
+
m2m_realised: z.number().optional(),
|
|
79
|
+
m2m_unrealised: z.number().optional(),
|
|
80
|
+
option_premium: z.number().optional(),
|
|
81
|
+
payout: z.number().optional(),
|
|
82
|
+
span: z.number().optional(),
|
|
83
|
+
holding_sales: z.number().optional(),
|
|
84
|
+
turnover: z.number().optional(),
|
|
85
|
+
liquid_collateral: z.number().optional(),
|
|
86
|
+
stock_collateral: z.number().optional(),
|
|
87
|
+
delivery: z.number().optional(),
|
|
88
|
+
})
|
|
89
|
+
.optional(),
|
|
90
|
+
});
|
|
91
|
+
export const MarginsSchema = z.looseObject({
|
|
92
|
+
equity: SegmentMarginSchema.optional(),
|
|
93
|
+
commodity: SegmentMarginSchema.optional(),
|
|
94
|
+
});
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Orders
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
/** Known terminal states. Not exhaustive — see the note at the top of this file. */
|
|
99
|
+
export const ORDER_STATUS = {
|
|
100
|
+
Complete: 'COMPLETE',
|
|
101
|
+
Cancelled: 'CANCELLED',
|
|
102
|
+
Rejected: 'REJECTED',
|
|
103
|
+
Open: 'OPEN',
|
|
104
|
+
TriggerPending: 'TRIGGER PENDING',
|
|
105
|
+
};
|
|
106
|
+
export const TERMINAL_ORDER_STATUSES = new Set([
|
|
107
|
+
ORDER_STATUS.Complete,
|
|
108
|
+
ORDER_STATUS.Cancelled,
|
|
109
|
+
ORDER_STATUS.Rejected,
|
|
110
|
+
]);
|
|
111
|
+
export const OrderSchema = z.looseObject({
|
|
112
|
+
order_id: z.string(),
|
|
113
|
+
parent_order_id: z.string().nullish(),
|
|
114
|
+
exchange_order_id: z.string().nullish(),
|
|
115
|
+
status: z.string(),
|
|
116
|
+
status_message: z.string().nullish(),
|
|
117
|
+
status_message_raw: z.string().nullish(),
|
|
118
|
+
order_timestamp: z.string().nullish(),
|
|
119
|
+
exchange_timestamp: z.string().nullish(),
|
|
120
|
+
exchange_update_timestamp: z.string().nullish(),
|
|
121
|
+
variety: z.string().optional(),
|
|
122
|
+
exchange: z.string().optional(),
|
|
123
|
+
tradingsymbol: z.string().optional(),
|
|
124
|
+
instrument_token: z.number().optional(),
|
|
125
|
+
transaction_type: z.string().optional(),
|
|
126
|
+
order_type: z.string().optional(),
|
|
127
|
+
product: z.string().optional(),
|
|
128
|
+
validity: z.string().optional(),
|
|
129
|
+
price: z.number().optional(),
|
|
130
|
+
quantity: z.number().optional(),
|
|
131
|
+
trigger_price: z.number().optional(),
|
|
132
|
+
average_price: z.number().optional(),
|
|
133
|
+
pending_quantity: z.number().optional(),
|
|
134
|
+
filled_quantity: z.number().optional(),
|
|
135
|
+
disclosed_quantity: z.number().optional(),
|
|
136
|
+
cancelled_quantity: z.number().optional(),
|
|
137
|
+
tag: z.string().nullish(),
|
|
138
|
+
tags: z.array(z.string()).nullish(),
|
|
139
|
+
placed_by: z.string().optional(),
|
|
140
|
+
guid: z.string().nullish(),
|
|
141
|
+
});
|
|
142
|
+
export const TradeSchema = z.looseObject({
|
|
143
|
+
trade_id: z.string(),
|
|
144
|
+
order_id: z.string(),
|
|
145
|
+
exchange_order_id: z.string().nullish(),
|
|
146
|
+
tradingsymbol: z.string().optional(),
|
|
147
|
+
exchange: z.string().optional(),
|
|
148
|
+
instrument_token: z.number().optional(),
|
|
149
|
+
transaction_type: z.string().optional(),
|
|
150
|
+
product: z.string().optional(),
|
|
151
|
+
average_price: z.number().optional(),
|
|
152
|
+
quantity: z.number().optional(),
|
|
153
|
+
fill_timestamp: z.string().nullish(),
|
|
154
|
+
exchange_timestamp: z.string().nullish(),
|
|
155
|
+
order_timestamp: z.string().nullish(),
|
|
156
|
+
});
|
|
157
|
+
/**
|
|
158
|
+
* Place-order response.
|
|
159
|
+
*
|
|
160
|
+
* With `autoslice=true` the shape changes from an object to an ARRAY of up to
|
|
161
|
+
* 10 entries, where success and failure can coexist in a single response. This
|
|
162
|
+
* union models that; callers must handle both.
|
|
163
|
+
*/
|
|
164
|
+
export const PlaceOrderResultSchema = z.union([
|
|
165
|
+
z.looseObject({ order_id: z.string() }),
|
|
166
|
+
z.array(z.union([
|
|
167
|
+
z.looseObject({ order_id: z.string() }),
|
|
168
|
+
z.looseObject({
|
|
169
|
+
error: z.looseObject({
|
|
170
|
+
code: z.number().optional(),
|
|
171
|
+
error_type: z.string().optional(),
|
|
172
|
+
message: z.string().optional(),
|
|
173
|
+
}),
|
|
174
|
+
}),
|
|
175
|
+
])),
|
|
176
|
+
]);
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Portfolio
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
export const HoldingSchema = z.looseObject({
|
|
181
|
+
tradingsymbol: z.string(),
|
|
182
|
+
exchange: z.string(),
|
|
183
|
+
instrument_token: z.number().optional(),
|
|
184
|
+
isin: z.string().optional(),
|
|
185
|
+
product: z.string().optional(),
|
|
186
|
+
quantity: z.number().default(0),
|
|
187
|
+
t1_quantity: z.number().default(0),
|
|
188
|
+
realised_quantity: z.number().default(0),
|
|
189
|
+
opening_quantity: z.number().default(0),
|
|
190
|
+
collateral_quantity: z.number().default(0),
|
|
191
|
+
authorised_quantity: z.number().default(0),
|
|
192
|
+
average_price: z.number().default(0),
|
|
193
|
+
last_price: z.number().default(0),
|
|
194
|
+
close_price: z.number().default(0),
|
|
195
|
+
pnl: z.number().default(0),
|
|
196
|
+
day_change: z.number().default(0),
|
|
197
|
+
day_change_percentage: z.number().default(0),
|
|
198
|
+
});
|
|
199
|
+
export const PositionSchema = z.looseObject({
|
|
200
|
+
tradingsymbol: z.string(),
|
|
201
|
+
exchange: z.string(),
|
|
202
|
+
instrument_token: z.number().optional(),
|
|
203
|
+
product: z.string().optional(),
|
|
204
|
+
quantity: z.number().default(0),
|
|
205
|
+
overnight_quantity: z.number().default(0),
|
|
206
|
+
multiplier: z.number().default(1),
|
|
207
|
+
average_price: z.number().default(0),
|
|
208
|
+
close_price: z.number().default(0),
|
|
209
|
+
last_price: z.number().default(0),
|
|
210
|
+
value: z.number().default(0),
|
|
211
|
+
pnl: z.number().default(0),
|
|
212
|
+
m2m: z.number().default(0),
|
|
213
|
+
unrealised: z.number().default(0),
|
|
214
|
+
realised: z.number().default(0),
|
|
215
|
+
buy_quantity: z.number().default(0),
|
|
216
|
+
buy_price: z.number().default(0),
|
|
217
|
+
buy_value: z.number().default(0),
|
|
218
|
+
sell_quantity: z.number().default(0),
|
|
219
|
+
sell_price: z.number().default(0),
|
|
220
|
+
sell_value: z.number().default(0),
|
|
221
|
+
day_buy_quantity: z.number().default(0),
|
|
222
|
+
day_sell_quantity: z.number().default(0),
|
|
223
|
+
});
|
|
224
|
+
export const PositionsSchema = z.looseObject({
|
|
225
|
+
net: z.array(PositionSchema).default([]),
|
|
226
|
+
day: z.array(PositionSchema).default([]),
|
|
227
|
+
});
|
|
228
|
+
export const AuctionSchema = z.looseObject({
|
|
229
|
+
tradingsymbol: z.string(),
|
|
230
|
+
exchange: z.string(),
|
|
231
|
+
instrument_token: z.number().optional(),
|
|
232
|
+
auction_number: z.string().optional(),
|
|
233
|
+
quantity: z.number().optional(),
|
|
234
|
+
last_price: z.number().optional(),
|
|
235
|
+
});
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Market data
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
const DepthEntrySchema = z.looseObject({
|
|
240
|
+
price: z.number().default(0),
|
|
241
|
+
quantity: z.number().default(0),
|
|
242
|
+
orders: z.number().default(0),
|
|
243
|
+
});
|
|
244
|
+
export const OhlcSchema = z.looseObject({
|
|
245
|
+
open: z.number().default(0),
|
|
246
|
+
high: z.number().default(0),
|
|
247
|
+
low: z.number().default(0),
|
|
248
|
+
close: z.number().default(0),
|
|
249
|
+
});
|
|
250
|
+
export const QuoteSchema = z.looseObject({
|
|
251
|
+
instrument_token: z.number(),
|
|
252
|
+
timestamp: z.string().nullish(),
|
|
253
|
+
last_trade_time: z.string().nullish(),
|
|
254
|
+
last_price: z.number().default(0),
|
|
255
|
+
last_quantity: z.number().optional(),
|
|
256
|
+
buy_quantity: z.number().optional(),
|
|
257
|
+
sell_quantity: z.number().optional(),
|
|
258
|
+
volume: z.number().optional(),
|
|
259
|
+
average_price: z.number().optional(),
|
|
260
|
+
oi: z.number().optional(),
|
|
261
|
+
oi_day_high: z.number().optional(),
|
|
262
|
+
oi_day_low: z.number().optional(),
|
|
263
|
+
net_change: z.number().optional(),
|
|
264
|
+
lower_circuit_limit: z.number().optional(),
|
|
265
|
+
upper_circuit_limit: z.number().optional(),
|
|
266
|
+
ohlc: OhlcSchema.optional(),
|
|
267
|
+
depth: z
|
|
268
|
+
.looseObject({
|
|
269
|
+
buy: z.array(DepthEntrySchema).default([]),
|
|
270
|
+
sell: z.array(DepthEntrySchema).default([]),
|
|
271
|
+
})
|
|
272
|
+
.optional(),
|
|
273
|
+
});
|
|
274
|
+
export const LtpQuoteSchema = z.looseObject({
|
|
275
|
+
instrument_token: z.number(),
|
|
276
|
+
last_price: z.number().default(0),
|
|
277
|
+
});
|
|
278
|
+
export const OhlcQuoteSchema = z.looseObject({
|
|
279
|
+
instrument_token: z.number(),
|
|
280
|
+
last_price: z.number().default(0),
|
|
281
|
+
ohlc: OhlcSchema.optional(),
|
|
282
|
+
});
|
|
283
|
+
/**
|
|
284
|
+
* Quote endpoints return a map keyed by "EXCHANGE:TRADINGSYMBOL". Instruments
|
|
285
|
+
* with no data — expired, invalid, or never traded — are simply ABSENT from the
|
|
286
|
+
* map rather than present with nulls. Callers must never assume presence.
|
|
287
|
+
*/
|
|
288
|
+
export const QuoteMapSchema = z.record(z.string(), QuoteSchema);
|
|
289
|
+
export const LtpMapSchema = z.record(z.string(), LtpQuoteSchema);
|
|
290
|
+
export const OhlcMapSchema = z.record(z.string(), OhlcQuoteSchema);
|
|
291
|
+
/**
|
|
292
|
+
* Historical candles: [timestamp, open, high, low, close, volume] with an
|
|
293
|
+
* optional 7th open-interest element when oi=1 was requested.
|
|
294
|
+
*/
|
|
295
|
+
export const CandleSchema = z.tuple([z.string(), z.number(), z.number(), z.number(), z.number(), z.number()], z.number());
|
|
296
|
+
export const CandlesSchema = z.looseObject({
|
|
297
|
+
candles: z.array(CandleSchema).default([]),
|
|
298
|
+
});
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// GTT
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
export const GttSchema = z.looseObject({
|
|
303
|
+
id: z.number(),
|
|
304
|
+
user_id: z.string().optional(),
|
|
305
|
+
parent_trigger: z.unknown().nullish(),
|
|
306
|
+
type: z.string(),
|
|
307
|
+
created_at: z.string().optional(),
|
|
308
|
+
updated_at: z.string().optional(),
|
|
309
|
+
expires_at: z.string().optional(),
|
|
310
|
+
status: z.string(),
|
|
311
|
+
condition: z.looseObject({
|
|
312
|
+
exchange: z.string(),
|
|
313
|
+
tradingsymbol: z.string(),
|
|
314
|
+
trigger_values: z.array(z.number()).default([]),
|
|
315
|
+
last_price: z.number().optional(),
|
|
316
|
+
instrument_token: z.number().optional(),
|
|
317
|
+
}),
|
|
318
|
+
orders: z
|
|
319
|
+
.array(z.looseObject({
|
|
320
|
+
exchange: z.string().optional(),
|
|
321
|
+
tradingsymbol: z.string().optional(),
|
|
322
|
+
product: z.string().optional(),
|
|
323
|
+
order_type: z.string().optional(),
|
|
324
|
+
transaction_type: z.string().optional(),
|
|
325
|
+
quantity: z.number().optional(),
|
|
326
|
+
price: z.number().optional(),
|
|
327
|
+
result: z.unknown().nullish(),
|
|
328
|
+
}))
|
|
329
|
+
.default([]),
|
|
330
|
+
});
|
|
331
|
+
export const GttCreateResultSchema = z.looseObject({ trigger_id: z.number() });
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// Margins / charges calculator
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
export const OrderMarginSchema = z.looseObject({
|
|
336
|
+
type: z.string().optional(),
|
|
337
|
+
tradingsymbol: z.string().optional(),
|
|
338
|
+
exchange: z.string().optional(),
|
|
339
|
+
span: z.number().optional(),
|
|
340
|
+
exposure: z.number().optional(),
|
|
341
|
+
option_premium: z.number().optional(),
|
|
342
|
+
additional: z.number().optional(),
|
|
343
|
+
bo: z.number().optional(),
|
|
344
|
+
cash: z.number().optional(),
|
|
345
|
+
var: z.number().optional(),
|
|
346
|
+
total: z.number().optional(),
|
|
347
|
+
leverage: z.number().optional(),
|
|
348
|
+
charges: z
|
|
349
|
+
.looseObject({
|
|
350
|
+
transaction_tax: z.number().optional(),
|
|
351
|
+
transaction_tax_type: z.string().optional(),
|
|
352
|
+
exchange_turnover_charge: z.number().optional(),
|
|
353
|
+
sebi_turnover_charge: z.number().optional(),
|
|
354
|
+
brokerage: z.number().optional(),
|
|
355
|
+
stamp_duty: z.number().optional(),
|
|
356
|
+
total: z.number().optional(),
|
|
357
|
+
gst: z
|
|
358
|
+
.looseObject({
|
|
359
|
+
igst: z.number().optional(),
|
|
360
|
+
cgst: z.number().optional(),
|
|
361
|
+
sgst: z.number().optional(),
|
|
362
|
+
total: z.number().optional(),
|
|
363
|
+
})
|
|
364
|
+
.optional(),
|
|
365
|
+
})
|
|
366
|
+
.optional(),
|
|
367
|
+
pnl: z
|
|
368
|
+
.looseObject({
|
|
369
|
+
realised: z.number().optional(),
|
|
370
|
+
unrealised: z.number().optional(),
|
|
371
|
+
})
|
|
372
|
+
.optional(),
|
|
373
|
+
});
|
|
374
|
+
export const BasketMarginSchema = z.looseObject({
|
|
375
|
+
initial: OrderMarginSchema.optional(),
|
|
376
|
+
final: OrderMarginSchema.optional(),
|
|
377
|
+
orders: z.array(OrderMarginSchema).default([]),
|
|
378
|
+
charges: z.unknown().optional(),
|
|
379
|
+
});
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// Mutual funds (read-only: the current docs state order placement is not
|
|
382
|
+
// available over the API, since it needs a bank debit)
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
export const MfHoldingSchema = z.looseObject({
|
|
385
|
+
folio: z.string().nullish(),
|
|
386
|
+
fund: z.string().optional(),
|
|
387
|
+
tradingsymbol: z.string(),
|
|
388
|
+
average_price: z.number().default(0),
|
|
389
|
+
last_price: z.number().default(0),
|
|
390
|
+
last_price_date: z.string().optional(),
|
|
391
|
+
pnl: z.number().default(0),
|
|
392
|
+
quantity: z.number().default(0),
|
|
393
|
+
});
|
|
394
|
+
export const MfOrderSchema = z.looseObject({
|
|
395
|
+
order_id: z.string(),
|
|
396
|
+
fund: z.string().optional(),
|
|
397
|
+
tradingsymbol: z.string().optional(),
|
|
398
|
+
status: z.string().optional(),
|
|
399
|
+
status_message: z.string().nullish(),
|
|
400
|
+
folio: z.string().nullish(),
|
|
401
|
+
order_timestamp: z.string().nullish(),
|
|
402
|
+
transaction_type: z.string().optional(),
|
|
403
|
+
quantity: z.number().nullish(),
|
|
404
|
+
amount: z.number().nullish(),
|
|
405
|
+
average_price: z.number().nullish(),
|
|
406
|
+
});
|
|
407
|
+
export const MfSipSchema = z.looseObject({
|
|
408
|
+
sip_id: z.string(),
|
|
409
|
+
tradingsymbol: z.string().optional(),
|
|
410
|
+
fund: z.string().optional(),
|
|
411
|
+
status: z.string().optional(),
|
|
412
|
+
instalment_amount: z.number().optional(),
|
|
413
|
+
instalments: z.number().optional(),
|
|
414
|
+
frequency: z.string().optional(),
|
|
415
|
+
next_instalment: z.string().nullish(),
|
|
416
|
+
});
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Instruments (parsed from the daily CSV dump)
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
export const InstrumentSchema = z.looseObject({
|
|
421
|
+
instrument_token: z.number(),
|
|
422
|
+
exchange_token: z.number().optional(),
|
|
423
|
+
tradingsymbol: z.string(),
|
|
424
|
+
name: z.string().optional(),
|
|
425
|
+
last_price: z.number().optional(),
|
|
426
|
+
expiry: z.string().optional(),
|
|
427
|
+
strike: z.number().optional(),
|
|
428
|
+
tick_size: z.number().optional(),
|
|
429
|
+
lot_size: z.number().optional(),
|
|
430
|
+
instrument_type: z.string().optional(),
|
|
431
|
+
segment: z.string().optional(),
|
|
432
|
+
exchange: z.string(),
|
|
433
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function encryptToFile(secrets: Record<string, string>, passphrase: string): Promise<void>;
|
|
2
|
+
export declare function decryptFromFile(passphrase: string): Promise<Record<string, string> | null>;
|
|
3
|
+
export declare function deleteCredentialFile(): Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Constant-time string comparison, for any future passphrase confirmation.
|
|
6
|
+
*
|
|
7
|
+
* Length is compared on the encoded buffers, not on String.length —
|
|
8
|
+
* timingSafeEqual throws RangeError on mismatched byte lengths, and UTF-16
|
|
9
|
+
* code-unit counts can match while byte lengths do not.
|
|
10
|
+
*/
|
|
11
|
+
export declare function safeEqual(a: string, b: string): boolean;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scrypt as scryptCb, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { chmod, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { ExitCode, KiteCliError } from './errors.js';
|
|
5
|
+
import { configDir, credentialsFile, ensurePrivateDir } from './paths.js';
|
|
6
|
+
const scrypt = promisify(scryptCb);
|
|
7
|
+
/**
|
|
8
|
+
* Encrypted-file credential store.
|
|
9
|
+
*
|
|
10
|
+
* This is the fallback for machines with no usable OS keyring — headless Linux
|
|
11
|
+
* without D-Bus, containers, some CI runners. It is strictly worse than the
|
|
12
|
+
* keyring (the passphrase has to come from somewhere) but strictly better than
|
|
13
|
+
* the plaintext files that gh, aws, and stripe all fall back to.
|
|
14
|
+
*
|
|
15
|
+
* Format: a JSON header, a newline, then base64 ciphertext.
|
|
16
|
+
* The header is bound as AES-GCM additional authenticated data, so an attacker
|
|
17
|
+
* cannot downgrade the KDF parameters without failing decryption.
|
|
18
|
+
*/
|
|
19
|
+
/** OWASP-recommended scrypt parameters. ~270ms on an M-series laptop. */
|
|
20
|
+
const KDF = {
|
|
21
|
+
N: 2 ** 17,
|
|
22
|
+
r: 8,
|
|
23
|
+
p: 1,
|
|
24
|
+
keylen: 32,
|
|
25
|
+
// Node's default maxmem is 32MB; scrypt at N=2^17,r=8 needs 128*N*r = 134MB
|
|
26
|
+
// and throws without this. A silent, confusing failure if you miss it.
|
|
27
|
+
maxmem: 256 * 1024 * 1024,
|
|
28
|
+
};
|
|
29
|
+
async function deriveKey(passphrase, salt, params) {
|
|
30
|
+
return scrypt(passphrase, salt, KDF.keylen, {
|
|
31
|
+
N: params.N,
|
|
32
|
+
r: params.r,
|
|
33
|
+
p: params.p,
|
|
34
|
+
maxmem: KDF.maxmem,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export async function encryptToFile(secrets, passphrase) {
|
|
38
|
+
const salt = randomBytes(16);
|
|
39
|
+
const nonce = randomBytes(12);
|
|
40
|
+
const key = await deriveKey(passphrase, salt, KDF);
|
|
41
|
+
const header = {
|
|
42
|
+
v: 1,
|
|
43
|
+
kdf: 'scrypt',
|
|
44
|
+
N: KDF.N,
|
|
45
|
+
r: KDF.r,
|
|
46
|
+
p: KDF.p,
|
|
47
|
+
salt: salt.toString('base64'),
|
|
48
|
+
nonce: nonce.toString('base64'),
|
|
49
|
+
};
|
|
50
|
+
const headerJson = JSON.stringify(header);
|
|
51
|
+
const cipher = createCipheriv('aes-256-gcm', key, nonce);
|
|
52
|
+
// Bind the header so KDF parameters cannot be tampered with.
|
|
53
|
+
cipher.setAAD(Buffer.from(headerJson, 'utf8'));
|
|
54
|
+
const ciphertext = Buffer.concat([cipher.update(Buffer.from(JSON.stringify(secrets), 'utf8')), cipher.final()]);
|
|
55
|
+
const tag = cipher.getAuthTag();
|
|
56
|
+
const payload = `${headerJson}\n${Buffer.concat([ciphertext, tag]).toString('base64')}\n`;
|
|
57
|
+
await ensurePrivateDir(configDir());
|
|
58
|
+
const path = credentialsFile();
|
|
59
|
+
await writeFile(path, payload, { mode: 0o600, encoding: 'utf8' });
|
|
60
|
+
if (process.platform !== 'win32') {
|
|
61
|
+
await chmod(path, 0o600);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function decryptFromFile(passphrase) {
|
|
65
|
+
let raw;
|
|
66
|
+
try {
|
|
67
|
+
raw = await readFile(credentialsFile(), 'utf8');
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err.code === 'ENOENT')
|
|
71
|
+
return null;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
const newlineAt = raw.indexOf('\n');
|
|
75
|
+
if (newlineAt === -1) {
|
|
76
|
+
throw new KiteCliError('Credential file is corrupt (no header).', ExitCode.Failure);
|
|
77
|
+
}
|
|
78
|
+
const headerJson = raw.slice(0, newlineAt);
|
|
79
|
+
const body = raw.slice(newlineAt + 1).trim();
|
|
80
|
+
let header;
|
|
81
|
+
try {
|
|
82
|
+
header = JSON.parse(headerJson);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new KiteCliError('Credential file is corrupt (bad header).', ExitCode.Failure);
|
|
86
|
+
}
|
|
87
|
+
if (header.v !== 1 || header.kdf !== 'scrypt') {
|
|
88
|
+
throw new KiteCliError(`Unsupported credential file version. Re-run \`kite login\` to rewrite it.`, ExitCode.Failure);
|
|
89
|
+
}
|
|
90
|
+
const salt = Buffer.from(header.salt, 'base64');
|
|
91
|
+
const nonce = Buffer.from(header.nonce, 'base64');
|
|
92
|
+
const key = await deriveKey(passphrase, salt, {
|
|
93
|
+
N: header.N,
|
|
94
|
+
r: header.r,
|
|
95
|
+
p: header.p,
|
|
96
|
+
});
|
|
97
|
+
const blob = Buffer.from(body, 'base64');
|
|
98
|
+
if (blob.length < 17) {
|
|
99
|
+
throw new KiteCliError('Credential file is corrupt (truncated).', ExitCode.Failure);
|
|
100
|
+
}
|
|
101
|
+
const ciphertext = blob.subarray(0, blob.length - 16);
|
|
102
|
+
const tag = blob.subarray(blob.length - 16);
|
|
103
|
+
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
|
|
104
|
+
decipher.setAAD(Buffer.from(headerJson, 'utf8'));
|
|
105
|
+
decipher.setAuthTag(tag);
|
|
106
|
+
try {
|
|
107
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
108
|
+
return JSON.parse(plaintext.toString('utf8'));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// GCM authentication failed: wrong passphrase, or the file was tampered
|
|
112
|
+
// with. We cannot distinguish the two, and should not try to.
|
|
113
|
+
throw new KiteCliError('Could not decrypt credentials — wrong passphrase, or the file was modified.', ExitCode.Auth, 'Re-run `kite login` to rewrite the credential file.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function deleteCredentialFile() {
|
|
117
|
+
try {
|
|
118
|
+
await unlink(credentialsFile());
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (err.code !== 'ENOENT')
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Constant-time string comparison, for any future passphrase confirmation.
|
|
127
|
+
*
|
|
128
|
+
* Length is compared on the encoded buffers, not on String.length —
|
|
129
|
+
* timingSafeEqual throws RangeError on mismatched byte lengths, and UTF-16
|
|
130
|
+
* code-unit counts can match while byte lengths do not.
|
|
131
|
+
*/
|
|
132
|
+
export function safeEqual(a, b) {
|
|
133
|
+
const bufA = Buffer.from(a, 'utf8');
|
|
134
|
+
const bufB = Buffer.from(b, 'utf8');
|
|
135
|
+
if (bufA.length !== bufB.length)
|
|
136
|
+
return false;
|
|
137
|
+
return timingSafeEqual(bufA, bufB);
|
|
138
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Non-secret session metadata.
|
|
4
|
+
*
|
|
5
|
+
* The access token itself lives in the OS keyring; only its expiry and the
|
|
6
|
+
* identity it belongs to are stored here. That split lets `kite whoami` show
|
|
7
|
+
* who you are and when your session dies without unlocking the keyring, and
|
|
8
|
+
* lets `kite logout` drop the token without touching the API secret.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SessionMetaSchema: z.ZodObject<{
|
|
11
|
+
userId: z.ZodString;
|
|
12
|
+
userName: z.ZodOptional<z.ZodString>;
|
|
13
|
+
broker: z.ZodOptional<z.ZodString>;
|
|
14
|
+
env: z.ZodString;
|
|
15
|
+
apiKey: z.ZodString;
|
|
16
|
+
expiresAt: z.ZodString;
|
|
17
|
+
loginTime: z.ZodOptional<z.ZodString>;
|
|
18
|
+
exchanges: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
19
|
+
products: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type SessionMeta = z.infer<typeof SessionMetaSchema>;
|
|
22
|
+
export declare function loadSessionMeta(): Promise<SessionMeta | null>;
|
|
23
|
+
export declare function saveSessionMeta(meta: SessionMeta): Promise<void>;
|
|
24
|
+
export declare function clearSessionMeta(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Kite access tokens expire at 06:00 IST the following day — a regulatory
|
|
27
|
+
* requirement, not a configurable session length.
|
|
28
|
+
*
|
|
29
|
+
* This is a floor, not a guarantee: a master logout from Kite web invalidates
|
|
30
|
+
* the token immediately, and there is no way to detect that except by getting a
|
|
31
|
+
* 403. Callers must treat TokenException as authoritative regardless of what
|
|
32
|
+
* this says.
|
|
33
|
+
*/
|
|
34
|
+
export declare function nextTokenExpiry(now?: Date): Date;
|
|
35
|
+
export declare function isExpired(meta: SessionMeta, now?: Date): boolean;
|
|
36
|
+
/** Human-readable time remaining, e.g. "4h 12m". */
|
|
37
|
+
export declare function timeUntilExpiry(meta: SessionMeta, now?: Date): string;
|