@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,114 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* User configuration, persisted at ~/.config/kite/config.json.
|
|
4
|
+
*
|
|
5
|
+
* Note what is deliberately absent: there is no `yes` / `assumeYes` setting.
|
|
6
|
+
* Bypassing a confirmation prompt must be explicit at the call site every time,
|
|
7
|
+
* so `--yes` is a flag only. A config file that silently disables confirmations
|
|
8
|
+
* is exactly how an accidental order gets placed.
|
|
9
|
+
*/
|
|
10
|
+
export declare const EnvironmentSchema: z.ZodEnum<{
|
|
11
|
+
production: "production";
|
|
12
|
+
sandbox: "sandbox";
|
|
13
|
+
}>;
|
|
14
|
+
export type Environment = z.infer<typeof EnvironmentSchema>;
|
|
15
|
+
export declare const ConfigSchema: z.ZodObject<{
|
|
16
|
+
apiKey: z.ZodOptional<z.ZodString>;
|
|
17
|
+
env: z.ZodDefault<z.ZodEnum<{
|
|
18
|
+
production: "production";
|
|
19
|
+
sandbox: "sandbox";
|
|
20
|
+
}>>;
|
|
21
|
+
trading: z.ZodPrefault<z.ZodObject<{
|
|
22
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
23
|
+
confirm: z.ZodDefault<z.ZodBoolean>;
|
|
24
|
+
maxOrderValue: z.ZodOptional<z.ZodNumber>;
|
|
25
|
+
strictConfirmAbove: z.ZodDefault<z.ZodNumber>;
|
|
26
|
+
}, z.core.$strip>>;
|
|
27
|
+
output: z.ZodPrefault<z.ZodObject<{
|
|
28
|
+
color: z.ZodDefault<z.ZodEnum<{
|
|
29
|
+
auto: "auto";
|
|
30
|
+
always: "always";
|
|
31
|
+
never: "never";
|
|
32
|
+
}>>;
|
|
33
|
+
compact: z.ZodDefault<z.ZodBoolean>;
|
|
34
|
+
}, z.core.$strip>>;
|
|
35
|
+
redirectPort: z.ZodDefault<z.ZodNumber>;
|
|
36
|
+
redirectPath: z.ZodDefault<z.ZodString>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
39
|
+
export declare function defaultConfig(): Config;
|
|
40
|
+
export declare function loadConfig(): Promise<Config>;
|
|
41
|
+
export declare function saveConfig(config: Config): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* The settings `kite config set` accepts.
|
|
44
|
+
*
|
|
45
|
+
* Declared explicitly rather than inferred from the schema, because optional
|
|
46
|
+
* settings with no default (maxOrderValue) are indistinguishable from unknown
|
|
47
|
+
* keys otherwise. This also drives value coercion and `--help` text.
|
|
48
|
+
*
|
|
49
|
+
* Note what is absent: there is no key for skipping confirmations. `--yes`
|
|
50
|
+
* must be passed at the call site every time.
|
|
51
|
+
*/
|
|
52
|
+
export declare const SETTABLE_KEYS: {
|
|
53
|
+
readonly apiKey: {
|
|
54
|
+
readonly type: "string";
|
|
55
|
+
readonly description: "Kite Connect API key";
|
|
56
|
+
};
|
|
57
|
+
readonly env: {
|
|
58
|
+
readonly type: "string";
|
|
59
|
+
readonly description: "production or sandbox";
|
|
60
|
+
};
|
|
61
|
+
readonly 'trading.enabled': {
|
|
62
|
+
readonly type: "boolean";
|
|
63
|
+
readonly description: "Master kill switch for all order commands";
|
|
64
|
+
};
|
|
65
|
+
readonly 'trading.confirm': {
|
|
66
|
+
readonly type: "boolean";
|
|
67
|
+
readonly description: "Require confirmation before money-moving actions";
|
|
68
|
+
};
|
|
69
|
+
readonly 'trading.maxOrderValue': {
|
|
70
|
+
readonly type: "number";
|
|
71
|
+
readonly description: "Refuse any single order above this rupee value";
|
|
72
|
+
};
|
|
73
|
+
readonly 'trading.strictConfirmAbove': {
|
|
74
|
+
readonly type: "number";
|
|
75
|
+
readonly description: "Above this rupee value, require typing the symbol to confirm";
|
|
76
|
+
};
|
|
77
|
+
readonly 'output.color': {
|
|
78
|
+
readonly type: "string";
|
|
79
|
+
readonly description: "auto, always, or never";
|
|
80
|
+
};
|
|
81
|
+
readonly 'output.compact': {
|
|
82
|
+
readonly type: "boolean";
|
|
83
|
+
readonly description: "Render tables without borders";
|
|
84
|
+
};
|
|
85
|
+
readonly redirectPort: {
|
|
86
|
+
readonly type: "number";
|
|
87
|
+
readonly description: "Loopback port for the login callback";
|
|
88
|
+
};
|
|
89
|
+
readonly redirectPath: {
|
|
90
|
+
readonly type: "string";
|
|
91
|
+
readonly description: "Path component of the login callback URL";
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
export type SettableKey = keyof typeof SETTABLE_KEYS;
|
|
95
|
+
export declare function isSettableKey(key: string): key is SettableKey;
|
|
96
|
+
/** Environment-specific API endpoints. */
|
|
97
|
+
export interface Endpoints {
|
|
98
|
+
api: string;
|
|
99
|
+
ws: string;
|
|
100
|
+
login: string;
|
|
101
|
+
/**
|
|
102
|
+
* Sandbox serves every route under an /oms prefix — except /instruments.
|
|
103
|
+
* Production has no prefix.
|
|
104
|
+
*/
|
|
105
|
+
routePrefix: string;
|
|
106
|
+
}
|
|
107
|
+
export declare function endpointsFor(env: Environment): Endpoints;
|
|
108
|
+
/** Public sandbox credentials, documented at https://kite.trade/docs/connect/v3/sandbox/ */
|
|
109
|
+
export declare const SANDBOX_CREDENTIALS: {
|
|
110
|
+
readonly apiKey: "sandboxdemo";
|
|
111
|
+
readonly apiSecret: "sandboxdemo-secret";
|
|
112
|
+
};
|
|
113
|
+
/** Resolve the effective environment: --env flag > KITE_ENV > config > production. */
|
|
114
|
+
export declare function resolveEnv(flag: string | undefined, config: Config): Environment;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { chmod, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ExitCode, KiteCliError } from './errors.js';
|
|
4
|
+
import { configDir, configFile, ensurePrivateDir } from './paths.js';
|
|
5
|
+
/**
|
|
6
|
+
* User configuration, persisted at ~/.config/kite/config.json.
|
|
7
|
+
*
|
|
8
|
+
* Note what is deliberately absent: there is no `yes` / `assumeYes` setting.
|
|
9
|
+
* Bypassing a confirmation prompt must be explicit at the call site every time,
|
|
10
|
+
* so `--yes` is a flag only. A config file that silently disables confirmations
|
|
11
|
+
* is exactly how an accidental order gets placed.
|
|
12
|
+
*/
|
|
13
|
+
export const EnvironmentSchema = z.enum(['production', 'sandbox']);
|
|
14
|
+
export const ConfigSchema = z.object({
|
|
15
|
+
/** Kite Connect API key. Semi-public (it appears in login URLs), so not a keyring secret. */
|
|
16
|
+
apiKey: z.string().min(1).optional(),
|
|
17
|
+
env: EnvironmentSchema.default('production'),
|
|
18
|
+
trading: z
|
|
19
|
+
.object({
|
|
20
|
+
/**
|
|
21
|
+
* Local kill switch. When false, every order-placing, order-modifying and
|
|
22
|
+
* GTT-mutating command refuses before touching the network.
|
|
23
|
+
*/
|
|
24
|
+
enabled: z.boolean().default(true),
|
|
25
|
+
/** Require an interactive confirmation before any money-moving action. */
|
|
26
|
+
confirm: z.boolean().default(true),
|
|
27
|
+
/**
|
|
28
|
+
* Refuse any single order whose notional value exceeds this (rupees).
|
|
29
|
+
* Undefined means no cap.
|
|
30
|
+
*/
|
|
31
|
+
maxOrderValue: z.number().positive().optional(),
|
|
32
|
+
/**
|
|
33
|
+
* Above this notional value, require typing a literal token to confirm
|
|
34
|
+
* rather than a single keystroke.
|
|
35
|
+
*/
|
|
36
|
+
strictConfirmAbove: z.number().positive().default(100_000),
|
|
37
|
+
})
|
|
38
|
+
.prefault({}),
|
|
39
|
+
output: z
|
|
40
|
+
.object({
|
|
41
|
+
color: z.enum(['auto', 'always', 'never']).default('auto'),
|
|
42
|
+
/** Default table style. */
|
|
43
|
+
compact: z.boolean().default(false),
|
|
44
|
+
})
|
|
45
|
+
.prefault({}),
|
|
46
|
+
/** Fixed loopback port for the OAuth redirect. Must match the developer console. */
|
|
47
|
+
redirectPort: z.number().int().min(1).max(65535).default(51101),
|
|
48
|
+
/** Path component of the redirect URL, e.g. "/callback". */
|
|
49
|
+
redirectPath: z.string().startsWith('/').default('/callback'),
|
|
50
|
+
});
|
|
51
|
+
export function defaultConfig() {
|
|
52
|
+
return ConfigSchema.parse({});
|
|
53
|
+
}
|
|
54
|
+
export async function loadConfig() {
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = await readFile(configFile(), 'utf8');
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err.code === 'ENOENT')
|
|
61
|
+
return defaultConfig();
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new KiteCliError(`Config file at ${configFile()} is not valid JSON.`, ExitCode.Failure, 'Fix it by hand, or delete it to start from defaults.');
|
|
70
|
+
}
|
|
71
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
throw new KiteCliError(`Config file at ${configFile()} is invalid:\n${z.prettifyError(result.error)}`, ExitCode.Failure);
|
|
74
|
+
}
|
|
75
|
+
return result.data;
|
|
76
|
+
}
|
|
77
|
+
export async function saveConfig(config) {
|
|
78
|
+
await ensurePrivateDir(configDir());
|
|
79
|
+
const path = configFile();
|
|
80
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, {
|
|
81
|
+
mode: 0o600,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
});
|
|
84
|
+
if (process.platform !== 'win32') {
|
|
85
|
+
await chmod(path, 0o600);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The settings `kite config set` accepts.
|
|
90
|
+
*
|
|
91
|
+
* Declared explicitly rather than inferred from the schema, because optional
|
|
92
|
+
* settings with no default (maxOrderValue) are indistinguishable from unknown
|
|
93
|
+
* keys otherwise. This also drives value coercion and `--help` text.
|
|
94
|
+
*
|
|
95
|
+
* Note what is absent: there is no key for skipping confirmations. `--yes`
|
|
96
|
+
* must be passed at the call site every time.
|
|
97
|
+
*/
|
|
98
|
+
export const SETTABLE_KEYS = {
|
|
99
|
+
apiKey: { type: 'string', description: 'Kite Connect API key' },
|
|
100
|
+
env: { type: 'string', description: 'production or sandbox' },
|
|
101
|
+
'trading.enabled': {
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
description: 'Master kill switch for all order commands',
|
|
104
|
+
},
|
|
105
|
+
'trading.confirm': {
|
|
106
|
+
type: 'boolean',
|
|
107
|
+
description: 'Require confirmation before money-moving actions',
|
|
108
|
+
},
|
|
109
|
+
'trading.maxOrderValue': {
|
|
110
|
+
type: 'number',
|
|
111
|
+
description: 'Refuse any single order above this rupee value',
|
|
112
|
+
},
|
|
113
|
+
'trading.strictConfirmAbove': {
|
|
114
|
+
type: 'number',
|
|
115
|
+
description: 'Above this rupee value, require typing the symbol to confirm',
|
|
116
|
+
},
|
|
117
|
+
'output.color': { type: 'string', description: 'auto, always, or never' },
|
|
118
|
+
'output.compact': {
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
description: 'Render tables without borders',
|
|
121
|
+
},
|
|
122
|
+
redirectPort: {
|
|
123
|
+
type: 'number',
|
|
124
|
+
description: 'Loopback port for the login callback',
|
|
125
|
+
},
|
|
126
|
+
redirectPath: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'Path component of the login callback URL',
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
export function isSettableKey(key) {
|
|
132
|
+
return Object.hasOwn(SETTABLE_KEYS, key);
|
|
133
|
+
}
|
|
134
|
+
export function endpointsFor(env) {
|
|
135
|
+
if (env === 'sandbox') {
|
|
136
|
+
return {
|
|
137
|
+
api: 'https://sandbox.kite.trade',
|
|
138
|
+
ws: 'wss://ws-sandbox.kite.trade',
|
|
139
|
+
login: 'https://sandbox.kite.trade/connect/login',
|
|
140
|
+
routePrefix: '/oms',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
api: 'https://api.kite.trade',
|
|
145
|
+
ws: 'wss://ws.kite.trade',
|
|
146
|
+
login: 'https://kite.zerodha.com/connect/login',
|
|
147
|
+
routePrefix: '',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Public sandbox credentials, documented at https://kite.trade/docs/connect/v3/sandbox/ */
|
|
151
|
+
export const SANDBOX_CREDENTIALS = {
|
|
152
|
+
apiKey: 'sandboxdemo',
|
|
153
|
+
apiSecret: 'sandboxdemo-secret',
|
|
154
|
+
};
|
|
155
|
+
/** Resolve the effective environment: --env flag > KITE_ENV > config > production. */
|
|
156
|
+
export function resolveEnv(flag, config) {
|
|
157
|
+
const candidate = flag ?? process.env['KITE_ENV'] ?? config.env;
|
|
158
|
+
const result = EnvironmentSchema.safeParse(candidate);
|
|
159
|
+
if (!result.success) {
|
|
160
|
+
throw new KiteCliError(`Unknown environment "${candidate}". Expected "production" or "sandbox".`, ExitCode.Usage);
|
|
161
|
+
}
|
|
162
|
+
return result.data;
|
|
163
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type SecretName = 'api_secret' | 'access_token';
|
|
2
|
+
export type Backend = 'env' | 'keyring' | 'file';
|
|
3
|
+
export interface CredentialLookup {
|
|
4
|
+
value: string;
|
|
5
|
+
backend: Backend;
|
|
6
|
+
}
|
|
7
|
+
/** True when an OS keyring is present and usable on this machine. */
|
|
8
|
+
export declare function keyringAvailable(): Promise<boolean>;
|
|
9
|
+
export interface CredentialStoreOptions {
|
|
10
|
+
env: string;
|
|
11
|
+
/** Passphrase for the file backend when the keyring is unavailable. */
|
|
12
|
+
passphrase?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read a secret, trying each backend in priority order.
|
|
16
|
+
*
|
|
17
|
+
* Every value returned is registered with the redactor, so it is scrubbed from
|
|
18
|
+
* any subsequent log, error, or stack trace even if it reaches an unexpected
|
|
19
|
+
* code path.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getSecret(name: SecretName, opts: CredentialStoreOptions): Promise<CredentialLookup | null>;
|
|
22
|
+
export declare function setSecret(name: SecretName, value: string, opts: CredentialStoreOptions): Promise<Backend>;
|
|
23
|
+
export declare function deleteSecret(name: SecretName, opts: CredentialStoreOptions): Promise<void>;
|
|
24
|
+
/** Remove every stored secret for an environment. Used by `kite logout --all`. */
|
|
25
|
+
export declare function deleteAllSecrets(opts: CredentialStoreOptions): Promise<void>;
|
|
26
|
+
/** True when secrets are being supplied entirely by the environment. */
|
|
27
|
+
export declare function usingEnvCredentials(): boolean;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { ExitCode, KiteCliError } from './errors.js';
|
|
2
|
+
import { registerSecret } from './redact.js';
|
|
3
|
+
import { decryptFromFile, deleteCredentialFile, encryptToFile } from './secretstore.js';
|
|
4
|
+
/**
|
|
5
|
+
* Layered credential storage.
|
|
6
|
+
*
|
|
7
|
+
* 1. Environment variables — always win, never persisted. This is what makes
|
|
8
|
+
* CI, Docker, and headless servers work (cf. gh's GH_TOKEN escape hatch).
|
|
9
|
+
* 2. OS keyring — macOS Keychain / Windows Credential Manager /
|
|
10
|
+
* Linux Secret Service, via @napi-rs/keyring.
|
|
11
|
+
* 3. Encrypted file — scrypt + AES-256-GCM at ~/.config/kite, 0600.
|
|
12
|
+
*
|
|
13
|
+
* keytar is deliberately not used: the repository was archived in 2022 and it
|
|
14
|
+
* depends on prebuild-install, an install-time network fetch that npm v12 now
|
|
15
|
+
* blocks by default.
|
|
16
|
+
*/
|
|
17
|
+
const SERVICE = 'kite-cli';
|
|
18
|
+
/** Env var that supplies each secret, bypassing all persistent storage. */
|
|
19
|
+
const ENV_VAR = {
|
|
20
|
+
api_secret: 'KITE_API_SECRET',
|
|
21
|
+
access_token: 'KITE_ACCESS_TOKEN',
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* `@napi-rs/keyring` is loaded lazily and defensively. On a headless Linux box
|
|
25
|
+
* with no D-Bus the *module* imports fine but every operation throws, so we
|
|
26
|
+
* probe on use rather than on import.
|
|
27
|
+
*/
|
|
28
|
+
let keyringModule;
|
|
29
|
+
async function loadKeyring() {
|
|
30
|
+
if (keyringModule !== undefined)
|
|
31
|
+
return keyringModule;
|
|
32
|
+
if (process.env['KITE_DISABLE_KEYRING'] === '1') {
|
|
33
|
+
keyringModule = null;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
keyringModule = await import('@napi-rs/keyring');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// No prebuilt binary for this platform, or the native module failed to
|
|
41
|
+
// load. Fall through to the encrypted file.
|
|
42
|
+
keyringModule = null;
|
|
43
|
+
}
|
|
44
|
+
return keyringModule;
|
|
45
|
+
}
|
|
46
|
+
async function keyringGet(account) {
|
|
47
|
+
const mod = await loadKeyring();
|
|
48
|
+
if (!mod)
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const entry = new mod.Entry(SERVICE, account);
|
|
52
|
+
// Note: @napi-rs/keyring returns null for a missing entry rather than
|
|
53
|
+
// throwing (unlike keytar). A bare try/catch would treat "absent" as
|
|
54
|
+
// success, so the null check is load-bearing.
|
|
55
|
+
return entry.getPassword() ?? null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function keyringSet(account, value) {
|
|
62
|
+
const mod = await loadKeyring();
|
|
63
|
+
if (!mod)
|
|
64
|
+
return false;
|
|
65
|
+
try {
|
|
66
|
+
new mod.Entry(SERVICE, account).setPassword(value);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function keyringDelete(account) {
|
|
74
|
+
const mod = await loadKeyring();
|
|
75
|
+
if (!mod)
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
new mod.Entry(SERVICE, account).deletePassword();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Nothing stored, or the keyring is locked. Either way there is nothing
|
|
82
|
+
// actionable for the caller.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** True when an OS keyring is present and usable on this machine. */
|
|
86
|
+
export async function keyringAvailable() {
|
|
87
|
+
const mod = await loadKeyring();
|
|
88
|
+
if (!mod)
|
|
89
|
+
return false;
|
|
90
|
+
try {
|
|
91
|
+
// A read against a name we never write. Succeeding (with null) proves the
|
|
92
|
+
// backend is reachable; throwing proves it is not.
|
|
93
|
+
new mod.Entry(SERVICE, '__probe__').getPassword();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Passphrase for the encrypted-file backend. Supplied via env, or prompted for
|
|
102
|
+
* interactively by the caller (which passes it in here).
|
|
103
|
+
*/
|
|
104
|
+
function filePassphrase() {
|
|
105
|
+
const value = process.env['KITE_CREDENTIALS_PASSPHRASE'];
|
|
106
|
+
return value && value !== '' ? value : null;
|
|
107
|
+
}
|
|
108
|
+
/** Namespaced account key, so sandbox and production sessions coexist. */
|
|
109
|
+
function accountKey(name, env) {
|
|
110
|
+
return env === 'production' ? name : `${env}:${name}`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Read a secret, trying each backend in priority order.
|
|
114
|
+
*
|
|
115
|
+
* Every value returned is registered with the redactor, so it is scrubbed from
|
|
116
|
+
* any subsequent log, error, or stack trace even if it reaches an unexpected
|
|
117
|
+
* code path.
|
|
118
|
+
*/
|
|
119
|
+
export async function getSecret(name, opts) {
|
|
120
|
+
const fromEnv = process.env[ENV_VAR[name]];
|
|
121
|
+
if (fromEnv && fromEnv.trim() !== '') {
|
|
122
|
+
registerSecret(fromEnv);
|
|
123
|
+
return { value: fromEnv, backend: 'env' };
|
|
124
|
+
}
|
|
125
|
+
const account = accountKey(name, opts.env);
|
|
126
|
+
const fromKeyring = await keyringGet(account);
|
|
127
|
+
if (fromKeyring) {
|
|
128
|
+
registerSecret(fromKeyring);
|
|
129
|
+
return { value: fromKeyring, backend: 'keyring' };
|
|
130
|
+
}
|
|
131
|
+
const passphrase = opts.passphrase ?? filePassphrase();
|
|
132
|
+
if (passphrase) {
|
|
133
|
+
const bag = await decryptFromFile(passphrase);
|
|
134
|
+
const value = bag?.[account];
|
|
135
|
+
if (value) {
|
|
136
|
+
registerSecret(value);
|
|
137
|
+
return { value, backend: 'file' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
export async function setSecret(name, value, opts) {
|
|
143
|
+
registerSecret(value);
|
|
144
|
+
const account = accountKey(name, opts.env);
|
|
145
|
+
if (await keyringSet(account, value)) {
|
|
146
|
+
return 'keyring';
|
|
147
|
+
}
|
|
148
|
+
const passphrase = opts.passphrase ?? filePassphrase();
|
|
149
|
+
if (!passphrase) {
|
|
150
|
+
throw new KiteCliError('No OS keyring is available on this machine and no passphrase was supplied.', ExitCode.Failure, 'Set KITE_CREDENTIALS_PASSPHRASE to enable the encrypted file store, or supply credentials via KITE_API_SECRET / KITE_ACCESS_TOKEN.');
|
|
151
|
+
}
|
|
152
|
+
const existing = (await decryptFromFile(passphrase)) ?? {};
|
|
153
|
+
existing[account] = value;
|
|
154
|
+
await encryptToFile(existing, passphrase);
|
|
155
|
+
return 'file';
|
|
156
|
+
}
|
|
157
|
+
export async function deleteSecret(name, opts) {
|
|
158
|
+
const account = accountKey(name, opts.env);
|
|
159
|
+
await keyringDelete(account);
|
|
160
|
+
const passphrase = opts.passphrase ?? filePassphrase();
|
|
161
|
+
if (passphrase) {
|
|
162
|
+
const existing = await decryptFromFile(passphrase);
|
|
163
|
+
if (existing && account in existing) {
|
|
164
|
+
delete existing[account];
|
|
165
|
+
if (Object.keys(existing).length === 0) {
|
|
166
|
+
await deleteCredentialFile();
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
await encryptToFile(existing, passphrase);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Remove every stored secret for an environment. Used by `kite logout --all`. */
|
|
175
|
+
export async function deleteAllSecrets(opts) {
|
|
176
|
+
await deleteSecret('access_token', opts);
|
|
177
|
+
await deleteSecret('api_secret', opts);
|
|
178
|
+
}
|
|
179
|
+
/** True when secrets are being supplied entirely by the environment. */
|
|
180
|
+
export function usingEnvCredentials() {
|
|
181
|
+
return Boolean(process.env[ENV_VAR.access_token] || process.env[ENV_VAR.api_secret]);
|
|
182
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error taxonomy for the Kite Connect API and this CLI.
|
|
3
|
+
*
|
|
4
|
+
* Kite returns errors as:
|
|
5
|
+
* { "status": "error", "message": "...", "error_type": "GeneralException" }
|
|
6
|
+
*
|
|
7
|
+
* See https://kite.trade/docs/connect/v3/exceptions/
|
|
8
|
+
*/
|
|
9
|
+
/** Process exit codes. Distinct per failure mode so scripts can branch on them. */
|
|
10
|
+
export declare const ExitCode: {
|
|
11
|
+
readonly Ok: 0;
|
|
12
|
+
/** Generic/unclassified failure. */
|
|
13
|
+
readonly Failure: 1;
|
|
14
|
+
/** Bad CLI usage: unknown flag, missing argument, invalid value. */
|
|
15
|
+
readonly Usage: 2;
|
|
16
|
+
/** No session, or the session expired. Run `kite login`. */
|
|
17
|
+
readonly Auth: 3;
|
|
18
|
+
/** Kite rejected the input (InputException). */
|
|
19
|
+
readonly Input: 4;
|
|
20
|
+
/** Order was rejected by the OMS (OrderException). */
|
|
21
|
+
readonly Order: 5;
|
|
22
|
+
/** Insufficient funds (MarginException). */
|
|
23
|
+
readonly Margin: 6;
|
|
24
|
+
/** Insufficient holdings to sell (HoldingException). */
|
|
25
|
+
readonly Holding: 7;
|
|
26
|
+
/** Rate limited (HTTP 429). */
|
|
27
|
+
readonly RateLimit: 8;
|
|
28
|
+
/** Kite or its upstream OMS is unreachable or erroring (5xx, network). */
|
|
29
|
+
readonly Upstream: 9;
|
|
30
|
+
/** The user declined a confirmation prompt. */
|
|
31
|
+
readonly Aborted: 10;
|
|
32
|
+
/** Confirmation required but stdin is not a TTY and --yes was not passed. */
|
|
33
|
+
readonly ConfirmationRequired: 11;
|
|
34
|
+
/** Holdings need depository authorisation (HTTP 428). */
|
|
35
|
+
readonly AuthorisationRequired: 12;
|
|
36
|
+
/** Trading is disabled by the local kill switch. */
|
|
37
|
+
readonly TradingDisabled: 13;
|
|
38
|
+
};
|
|
39
|
+
export type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode];
|
|
40
|
+
/** Kite's documented `error_type` values. */
|
|
41
|
+
export type KiteErrorType = 'TokenException' | 'UserException' | 'OrderException' | 'InputException' | 'MarginException' | 'HoldingException' | 'NetworkException' | 'DataException' | 'GeneralException';
|
|
42
|
+
/**
|
|
43
|
+
* Base class for every error this CLI raises deliberately. Carries an exit code
|
|
44
|
+
* and an optional remediation hint shown to the user.
|
|
45
|
+
*/
|
|
46
|
+
export declare class KiteCliError extends Error {
|
|
47
|
+
readonly exitCode: ExitCodeValue;
|
|
48
|
+
/** A short actionable next step, e.g. "Run `kite login`." */
|
|
49
|
+
readonly hint: string | undefined;
|
|
50
|
+
constructor(message: string, exitCode?: ExitCodeValue, hint?: string);
|
|
51
|
+
}
|
|
52
|
+
/** An error returned by the Kite API with a structured envelope. */
|
|
53
|
+
export declare class KiteApiError extends KiteCliError {
|
|
54
|
+
readonly status: number;
|
|
55
|
+
readonly errorType: KiteErrorType | string;
|
|
56
|
+
constructor(opts: {
|
|
57
|
+
message: string;
|
|
58
|
+
status: number;
|
|
59
|
+
errorType: KiteErrorType | string;
|
|
60
|
+
hint?: string;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** The local session is missing or expired. */
|
|
64
|
+
export declare class AuthRequiredError extends KiteCliError {
|
|
65
|
+
constructor(message?: string);
|
|
66
|
+
}
|
|
67
|
+
/** Bad CLI usage — distinct from Kite rejecting valid-looking input. */
|
|
68
|
+
export declare class UsageError extends KiteCliError {
|
|
69
|
+
constructor(message: string, hint?: string);
|
|
70
|
+
}
|
|
71
|
+
/** The user answered "no" at a confirmation prompt. */
|
|
72
|
+
export declare class AbortedError extends KiteCliError {
|
|
73
|
+
constructor(message?: string);
|
|
74
|
+
}
|
|
75
|
+
/** Network-level failure: DNS, TCP, TLS, or timeout. */
|
|
76
|
+
export declare class NetworkError extends KiteCliError {
|
|
77
|
+
constructor(message: string, hint?: string);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Remediation hints for the error types where the right next step is not
|
|
81
|
+
* obvious from Kite's own message.
|
|
82
|
+
*/
|
|
83
|
+
export declare function hintForApiError(status: number, errorType: string): string | undefined;
|