@ai-digital-factory/lmt-cli 0.1.2 → 0.1.4
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/README.md +68 -5
- package/dist/index.js +872 -135
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,13 +15,34 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// ../../packages/contracts/src/index.ts
|
|
18
|
-
|
|
18
|
+
function isOperatorMarketProtection(value) {
|
|
19
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 2 && value <= 90;
|
|
20
|
+
}
|
|
21
|
+
function validateEntryCategories(value) {
|
|
22
|
+
if (!Array.isArray(value) || value.some((category) => typeof category !== "string" || !ENTRY_CATEGORIES.includes(category)) || new Set(value).size !== value.length) {
|
|
23
|
+
throw new Error("entryCategories must be a list of supported Entry Categories with no duplicates.");
|
|
24
|
+
}
|
|
25
|
+
return [...value];
|
|
26
|
+
}
|
|
27
|
+
var OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS, OPERATOR_SESSION_EXPIRY_WARNING_MS, DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS, ZERODHA_SESSION_ADMISSION_HEADER, ZERODHA_SESSION_ADMISSION_VERIFY, ENTRY_CATEGORIES, API_PATHS;
|
|
19
28
|
var init_src = __esm({
|
|
20
29
|
"../../packages/contracts/src/index.ts"() {
|
|
21
30
|
"use strict";
|
|
22
31
|
OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS = 24 * 60 * 60;
|
|
23
32
|
OPERATOR_SESSION_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
|
|
24
33
|
DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS = 7 * 24 * 60 * 60;
|
|
34
|
+
ZERODHA_SESSION_ADMISSION_HEADER = "x-lmt-session-admission";
|
|
35
|
+
ZERODHA_SESSION_ADMISSION_VERIFY = "verify";
|
|
36
|
+
ENTRY_CATEGORIES = [
|
|
37
|
+
"NSE EQ",
|
|
38
|
+
"BSE EQ",
|
|
39
|
+
"NFO FUT",
|
|
40
|
+
"NFO CE",
|
|
41
|
+
"NFO PE",
|
|
42
|
+
"BFO FUT",
|
|
43
|
+
"BFO CE",
|
|
44
|
+
"BFO PE"
|
|
45
|
+
];
|
|
25
46
|
API_PATHS = {
|
|
26
47
|
health: "/api/health",
|
|
27
48
|
unlockOperatorSession: "/api/operator-session/unlock",
|
|
@@ -35,8 +56,12 @@ var init_src = __esm({
|
|
|
35
56
|
operatorLoginEvents: "/api/operators/login/events",
|
|
36
57
|
operatorLoginOtp: "/api/operators/login/otp",
|
|
37
58
|
cancelOperatorLogin: "/api/operators/login/cancel",
|
|
59
|
+
zerodhaAccessToken: "/api/zerodha/access-token",
|
|
60
|
+
zerodhaAccessTokenRecovery: "/api/zerodha/access-token/recovery",
|
|
61
|
+
zerodhaAccessTokenPublication: "/api/zerodha/access-token/publication",
|
|
38
62
|
zerodhaCredentials: "/api/zerodha/credentials",
|
|
39
63
|
entryCategorySettings: "/api/settings/entry-categories",
|
|
64
|
+
operatorSettings: "/api/settings",
|
|
40
65
|
marketProtectionSettings: "/api/settings/market-protection",
|
|
41
66
|
operatorEgress: "/api/operator-egress",
|
|
42
67
|
validateInstrument: "/api/instruments/validate",
|
|
@@ -48,6 +73,7 @@ var init_src = __esm({
|
|
|
48
73
|
previousInstruments: "/api/market-watch/previous-instruments",
|
|
49
74
|
candleHistoryLatest: "/api/candle-history/latest",
|
|
50
75
|
candleHistoryExport: "/api/candle-history/export",
|
|
76
|
+
candleExportOnDemand: "/api/candle-history/export/on-demand",
|
|
51
77
|
candleHistoryExportShare: "/api/candle-history/export/share",
|
|
52
78
|
candleHistory: "/api/candle-history",
|
|
53
79
|
zerodhaHistoricalCandles: "/api/zerodha/historical-candles",
|
|
@@ -111,7 +137,7 @@ function transportErrorDetail(error) {
|
|
|
111
137
|
}
|
|
112
138
|
return error.message;
|
|
113
139
|
}
|
|
114
|
-
async function requestOrThrow(request, unavailableMessage, signal, targetUrl) {
|
|
140
|
+
async function requestOrThrow(request, unavailableMessage, signal, targetUrl, secrets = []) {
|
|
115
141
|
try {
|
|
116
142
|
return await request();
|
|
117
143
|
} catch (error) {
|
|
@@ -119,7 +145,7 @@ async function requestOrThrow(request, unavailableMessage, signal, targetUrl) {
|
|
|
119
145
|
if (error instanceof OperatorSessionRejectedError || error instanceof ZerodhaLoginAttemptUnavailableError) throw error;
|
|
120
146
|
const detail = transportErrorDetail(error);
|
|
121
147
|
const at = targetUrl ? ` [${targetUrl}]` : "";
|
|
122
|
-
throw new Error(detail ? `${unavailableMessage} (${detail})${at}` : `${unavailableMessage}${at}
|
|
148
|
+
throw new Error(redactSecrets(detail ? `${unavailableMessage} (${detail})${at}` : `${unavailableMessage}${at}`, secrets));
|
|
123
149
|
}
|
|
124
150
|
}
|
|
125
151
|
function responseCookies(response) {
|
|
@@ -170,10 +196,18 @@ function parseEventFrame(frame) {
|
|
|
170
196
|
} catch {
|
|
171
197
|
throw new Error("Origin returned an invalid Zerodha login update.");
|
|
172
198
|
}
|
|
173
|
-
if (!event || typeof event !== "object" || !("type" in event)) {
|
|
199
|
+
if (!event || typeof event !== "object" || !("type" in event) || typeof event.attemptId !== "string" || !event.attemptId || "message" in event && typeof event.message !== "string" || event.type === "status" && (event.operatorId !== "OPERATOR_1" && event.operatorId !== "OPERATOR_2" || !["loading", "awaiting_otp", "succeeded", "failed"].includes(event.status) || event.failedOtpAttempts !== void 0 && (!Number.isInteger(event.failedOtpAttempts) || event.failedOtpAttempts < 0)) || event.type === "attempt_error" && (!["login_attempt_cancelled", "login_attempt_expired", "login_attempt_not_found"].includes(event.error) || typeof event.message !== "string") || event.type !== "status" && event.type !== "attempt_error") {
|
|
174
200
|
throw new Error("Origin returned an invalid Zerodha login update.");
|
|
175
201
|
}
|
|
176
|
-
return event;
|
|
202
|
+
if (event.type === "attempt_error") return { type: event.type, attemptId: event.attemptId, error: event.error, message: event.message };
|
|
203
|
+
return {
|
|
204
|
+
type: event.type,
|
|
205
|
+
attemptId: event.attemptId,
|
|
206
|
+
operatorId: event.operatorId,
|
|
207
|
+
status: event.status,
|
|
208
|
+
...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts },
|
|
209
|
+
...event.message === void 0 ? {} : { message: event.message }
|
|
210
|
+
};
|
|
177
211
|
}
|
|
178
212
|
function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
179
213
|
let cookies = { ...options.cookies ?? {} };
|
|
@@ -189,8 +223,7 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
189
223
|
return AbortSignal.any(present);
|
|
190
224
|
}
|
|
191
225
|
function sessionSecrets() {
|
|
192
|
-
|
|
193
|
-
return [cookies.operatorSessionCookie, cookies.operatorSessionCookie.slice(cookies.operatorSessionCookie.indexOf("=") + 1)];
|
|
226
|
+
return [cookies.operatorSessionCookie, cookies.clientIdentityCookie].filter((cookie) => Boolean(cookie)).flatMap((cookie) => [cookie, cookie.slice(cookie.indexOf("=") + 1)]);
|
|
194
227
|
}
|
|
195
228
|
function headers(extra) {
|
|
196
229
|
const result = new Headers(extra);
|
|
@@ -223,7 +256,7 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
223
256
|
throw new OperatorSessionRejectedError(rejectionReason(body));
|
|
224
257
|
}
|
|
225
258
|
}
|
|
226
|
-
async function postJson(path4, body, explicitSignal) {
|
|
259
|
+
async function postJson(path4, body, explicitSignal, secrets = []) {
|
|
227
260
|
const signal = actionSignal(explicitSignal);
|
|
228
261
|
const url = `${origin}${path4}`;
|
|
229
262
|
const response = await requestOrThrow(
|
|
@@ -235,33 +268,128 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
235
268
|
}),
|
|
236
269
|
"Could not reach the selected Live Market Terminal Origin.",
|
|
237
270
|
signal,
|
|
238
|
-
url
|
|
271
|
+
url,
|
|
272
|
+
[...sessionSecrets(), ...otpValues, ...secrets]
|
|
239
273
|
);
|
|
240
274
|
rememberCookies(response);
|
|
241
275
|
await rejectAbsentSession(response, path4);
|
|
242
276
|
return response;
|
|
243
277
|
}
|
|
244
|
-
async function getJson(path4, explicitSignal) {
|
|
278
|
+
async function getJson(path4, explicitSignal, extraHeaders) {
|
|
245
279
|
const signal = actionSignal(explicitSignal);
|
|
246
280
|
const url = `${origin}${path4}`;
|
|
247
281
|
const response = await requestOrThrow(
|
|
248
282
|
() => fetchImpl(url, {
|
|
249
283
|
method: "GET",
|
|
250
|
-
headers: headers({ accept: "application/json" }),
|
|
284
|
+
headers: headers({ accept: "application/json", ...extraHeaders }),
|
|
251
285
|
cache: "no-store",
|
|
252
286
|
...signal ? { signal } : {}
|
|
253
287
|
}),
|
|
254
288
|
"Could not reach the selected Live Market Terminal Origin.",
|
|
255
289
|
signal,
|
|
256
|
-
url
|
|
290
|
+
url,
|
|
291
|
+
[...sessionSecrets(), ...otpValues]
|
|
257
292
|
);
|
|
258
293
|
rememberCookies(response);
|
|
259
294
|
await rejectAbsentSession(response, path4);
|
|
260
295
|
return response;
|
|
261
296
|
}
|
|
297
|
+
async function settingsWrite(path4, body) {
|
|
298
|
+
let response;
|
|
299
|
+
try {
|
|
300
|
+
response = await fetchImpl(origin + path4, {
|
|
301
|
+
method: "PUT",
|
|
302
|
+
headers: headers({ "content-type": "application/json", accept: "application/json" }),
|
|
303
|
+
body: JSON.stringify(body),
|
|
304
|
+
signal: actionSignal()
|
|
305
|
+
});
|
|
306
|
+
} catch {
|
|
307
|
+
throw new SettingsActionError("submission_uncertain", true);
|
|
308
|
+
}
|
|
309
|
+
rememberCookies(response);
|
|
310
|
+
await rejectAbsentSession(response, path4);
|
|
311
|
+
if (!response.ok) {
|
|
312
|
+
const result = await readJson(response).catch(() => void 0);
|
|
313
|
+
const codes = [
|
|
314
|
+
"settings_conflict",
|
|
315
|
+
"settings_version_required",
|
|
316
|
+
"invalid_settings",
|
|
317
|
+
"trade_cycle_active",
|
|
318
|
+
"settings_save_failed",
|
|
319
|
+
"token_conflict",
|
|
320
|
+
"token_rejected",
|
|
321
|
+
"replacement_confirmation_required",
|
|
322
|
+
"token_write_pending",
|
|
323
|
+
"invalid_request",
|
|
324
|
+
"zerodha_session_absent",
|
|
325
|
+
"validation_failed",
|
|
326
|
+
"credentials_changed",
|
|
327
|
+
"replacement_blocked",
|
|
328
|
+
"token_changed",
|
|
329
|
+
"invalid_token_request",
|
|
330
|
+
"token_write_unresolved",
|
|
331
|
+
"active_trading_confirmation",
|
|
332
|
+
"validation_unavailable",
|
|
333
|
+
"unresolved_order",
|
|
334
|
+
"sharing_token_unavailable",
|
|
335
|
+
"token_recovery_unavailable",
|
|
336
|
+
"settings_recovery_required"
|
|
337
|
+
];
|
|
338
|
+
const code = typeof result?.error === "string" && codes.includes(result.error) ? result.error : "operation_rejected";
|
|
339
|
+
throw new SettingsActionError(code, code === "settings_save_failed" || response.status >= 500);
|
|
340
|
+
}
|
|
341
|
+
return response;
|
|
342
|
+
}
|
|
343
|
+
function ordinarySettings(body) {
|
|
344
|
+
if (!body || !Number.isSafeInteger(body.settingsVersion) || !Number.isInteger(body.rowCount) || body.rowCount < 1 || body.rowCount > 20 || !isOperatorMarketProtection(body.marketProtection) || !Array.isArray(body.entryCategories)) {
|
|
345
|
+
throw new SettingsActionError("invalid_response");
|
|
346
|
+
}
|
|
347
|
+
return { settingsVersion: body.settingsVersion, rowCount: body.rowCount, marketProtection: body.marketProtection, entryCategories: validateEntryCategories(body.entryCategories) };
|
|
348
|
+
}
|
|
262
349
|
const api = {
|
|
350
|
+
async getTokenMetadata() {
|
|
351
|
+
const response = await getJson(API_PATHS.zerodhaCredentials);
|
|
352
|
+
const body = await readJson(response);
|
|
353
|
+
if (!response.ok || !body?.accessTokenMetadata) throw new SettingsActionError("token_metadata_unavailable");
|
|
354
|
+
const { fingerprint, version, updatedAt, writePending } = body.accessTokenMetadata;
|
|
355
|
+
return { fingerprint, version, updatedAt, ...writePending === void 0 ? {} : { writePending } };
|
|
356
|
+
},
|
|
357
|
+
async getSettings() {
|
|
358
|
+
const response = await getJson(API_PATHS.operatorSettings);
|
|
359
|
+
if (!response.ok) throw new SettingsActionError("settings_read_failed");
|
|
360
|
+
return ordinarySettings(await readJson(response));
|
|
361
|
+
},
|
|
362
|
+
async saveSettings(request) {
|
|
363
|
+
const response = await settingsWrite(API_PATHS.operatorSettings, request);
|
|
364
|
+
try {
|
|
365
|
+
const body = await readJson(response);
|
|
366
|
+
if (body.outcome !== "saved" && body.outcome !== "unchanged") throw new Error("Invalid outcome");
|
|
367
|
+
return { outcome: body.outcome, settings: ordinarySettings(body.settings) };
|
|
368
|
+
} catch {
|
|
369
|
+
throw new SettingsActionError("submission_uncertain", true);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
async replaceToken(request) {
|
|
373
|
+
const response = await settingsWrite(API_PATHS.zerodhaAccessToken, request);
|
|
374
|
+
try {
|
|
375
|
+
const body = await readJson(response);
|
|
376
|
+
if (body.outcome !== "verified" && body.outcome !== "replaced") throw new Error("Invalid outcome");
|
|
377
|
+
return { outcome: body.outcome, sharing: body.sharing, metadata: body.metadata, message: "Token operation completed." };
|
|
378
|
+
} catch {
|
|
379
|
+
throw new SettingsActionError("submission_uncertain", true);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
async exportCandles(request) {
|
|
383
|
+
const response = await postJson(API_PATHS.candleExportOnDemand, request);
|
|
384
|
+
const body = await readJson(response);
|
|
385
|
+
if (!response.ok || !body || !("url" in body) || typeof body.url !== "string" || !("filename" in body)) {
|
|
386
|
+
throw new Error(messageFromBody(body, "Could not publish Candle Export.", sessionSecrets()));
|
|
387
|
+
}
|
|
388
|
+
return body;
|
|
389
|
+
},
|
|
263
390
|
setActionSignal(signal) {
|
|
264
391
|
actionDeadlineSignal = signal;
|
|
392
|
+
if (!signal) otpValues.clear();
|
|
265
393
|
},
|
|
266
394
|
setCookies(next) {
|
|
267
395
|
cookies = { ...next };
|
|
@@ -276,7 +404,7 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
276
404
|
return { ...metadata };
|
|
277
405
|
},
|
|
278
406
|
async unlock(username, password, signal) {
|
|
279
|
-
const response = await postJson(API_PATHS.unlockOperatorSession, { username, password }, signal);
|
|
407
|
+
const response = await postJson(API_PATHS.unlockOperatorSession, { username, password }, signal, [username, password]);
|
|
280
408
|
const body = await readJson(response);
|
|
281
409
|
if (!response.ok || !body || !("operatorSession" in body) || body.operatorSession !== "present") {
|
|
282
410
|
if (body && typeof body === "object" && "error" in body && body.error === "incorrect_credentials") {
|
|
@@ -366,8 +494,9 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
366
494
|
"Could not reach the selected Origin to start Login with Zerodha."
|
|
367
495
|
);
|
|
368
496
|
const body = await readJson(response);
|
|
369
|
-
if (!response.ok || !body || !("status" in body) || body.status !== "started" || typeof body.attemptId !== "string") {
|
|
370
|
-
|
|
497
|
+
if (!response.ok || !body || typeof body !== "object" || !("status" in body) || body.status !== "started" || typeof body.attemptId !== "string") {
|
|
498
|
+
const code = body && typeof body === "object" && "error" in body && typeof body.error === "string" && ["zerodha_credentials_missing", "secret_store_unavailable", "operator_not_configured"].includes(body.error) ? body.error : void 0;
|
|
499
|
+
throw Object.assign(new Error(messageFromBody(body, "Could not start Login with Zerodha.", sessionSecrets())), code ? { code } : {});
|
|
371
500
|
}
|
|
372
501
|
otpValues.clear();
|
|
373
502
|
return { attemptId: body.attemptId, operatorId: body.operatorId };
|
|
@@ -379,9 +508,9 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
379
508
|
"Could not reach the selected Origin to submit the OTP."
|
|
380
509
|
);
|
|
381
510
|
const body = await readJson(response);
|
|
382
|
-
if (!response.ok || !body || !("status" in body) || body.status !== "loading") {
|
|
511
|
+
if (!response.ok || !body || typeof body !== "object" || !("status" in body) || body.status !== "loading") {
|
|
383
512
|
const message = messageFromBody(body, "Could not submit the Zerodha OTP.", [...sessionSecrets(), ...otpValues]);
|
|
384
|
-
if (body && "error" in body && (body.error === "login_attempt_not_found" || body.error === "login_attempt_expired")) {
|
|
513
|
+
if (body && typeof body === "object" && "error" in body && (body.error === "login_attempt_not_found" || body.error === "login_attempt_expired")) {
|
|
385
514
|
throw new ZerodhaLoginAttemptUnavailableError(message);
|
|
386
515
|
}
|
|
387
516
|
throw new Error(message);
|
|
@@ -415,12 +544,22 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
415
544
|
throw new Error(message);
|
|
416
545
|
}
|
|
417
546
|
const reader = response.body.getReader();
|
|
547
|
+
const cancelReader = () => {
|
|
548
|
+
void reader.cancel().catch(() => void 0);
|
|
549
|
+
};
|
|
550
|
+
requestSignal.addEventListener("abort", cancelReader, { once: true });
|
|
418
551
|
const decoder = new TextDecoder();
|
|
419
552
|
let buffer = "";
|
|
420
553
|
let completed = false;
|
|
421
554
|
try {
|
|
422
555
|
while (!requestSignal.aborted) {
|
|
423
|
-
|
|
556
|
+
let chunk;
|
|
557
|
+
try {
|
|
558
|
+
chunk = await reader.read();
|
|
559
|
+
} catch {
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
const { done, value } = chunk;
|
|
424
563
|
buffer += decoder.decode(value, { stream: !done });
|
|
425
564
|
let boundary = buffer.search(/\r?\n\r?\n/);
|
|
426
565
|
while (boundary >= 0) {
|
|
@@ -450,6 +589,7 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
450
589
|
if (done) break;
|
|
451
590
|
}
|
|
452
591
|
} finally {
|
|
592
|
+
requestSignal.removeEventListener("abort", cancelReader);
|
|
453
593
|
await reader.cancel().catch(() => void 0);
|
|
454
594
|
}
|
|
455
595
|
if (completed || requestSignal.aborted) return;
|
|
@@ -458,24 +598,34 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
458
598
|
},
|
|
459
599
|
async confirmZerodhaSession() {
|
|
460
600
|
const response = await requestOrThrow(
|
|
461
|
-
() => getJson(API_PATHS.zerodhaSession),
|
|
601
|
+
() => getJson(API_PATHS.zerodhaSession, void 0, { [ZERODHA_SESSION_ADMISSION_HEADER]: ZERODHA_SESSION_ADMISSION_VERIFY }),
|
|
462
602
|
"Could not verify the Zerodha Session with the selected Origin."
|
|
463
603
|
);
|
|
464
604
|
const body = await readJson(response);
|
|
465
|
-
if (!response.ok || !body || !("zerodhaSession" in body) || body.zerodhaSession !== "present") {
|
|
466
|
-
|
|
605
|
+
if (!response.ok || !body || typeof body !== "object" || !("zerodhaSession" in body) || body.zerodhaSession !== "present") {
|
|
606
|
+
const code = body && typeof body === "object" && "error" in body && body.error === "zerodha_session_restoring" ? "verification" in body && body.verification === "unavailable" ? "zerodha_verification_unavailable" : "zerodha_session_restoring" : body && typeof body === "object" && "error" in body && body.error === "zerodha_session_absent" ? "zerodha_session_absent" : "zerodha_verification_unavailable";
|
|
607
|
+
throw Object.assign(new Error(messageFromBody(body, "Zerodha Session is not present on the Broker.", sessionSecrets())), { code });
|
|
467
608
|
}
|
|
468
609
|
return body;
|
|
469
610
|
}
|
|
470
611
|
};
|
|
471
612
|
return api;
|
|
472
613
|
}
|
|
473
|
-
var OperatorSessionRejectedError, ZerodhaLoginFailedError, ZerodhaLoginAttemptUnavailableError, OperatorCredentialsRejectedError;
|
|
614
|
+
var SettingsActionError, OperatorSessionRejectedError, ZerodhaLoginFailedError, ZerodhaLoginAttemptUnavailableError, OperatorCredentialsRejectedError;
|
|
474
615
|
var init_gate_api_client = __esm({
|
|
475
616
|
"src/gate-api-client.ts"() {
|
|
476
617
|
"use strict";
|
|
477
618
|
init_src();
|
|
478
619
|
init_profile_name();
|
|
620
|
+
SettingsActionError = class extends Error {
|
|
621
|
+
constructor(code, uncertain = false) {
|
|
622
|
+
super(uncertain ? "Submission could not be confirmed. Read current Settings or token metadata before another submission." : code === "settings_conflict" ? "Settings changed elsewhere. Read saved Settings and submit again with its version." : code === "active_trading_confirmation" ? "Another device has active trading. Review it, then explicitly submit continueReplacement: true to continue." : code === "token_changed" ? "The token changed. Read token metadata and review a fresh submission." : code === "token_write_unresolved" ? "A token write is unresolved. Resolve interrupted save in Web before replacing." : code === "unresolved_order" ? "An Order remains unresolved. Reconcile it before replacing the token." : "The Settings or token operation was rejected.");
|
|
623
|
+
this.code = code;
|
|
624
|
+
this.uncertain = uncertain;
|
|
625
|
+
}
|
|
626
|
+
code;
|
|
627
|
+
uncertain;
|
|
628
|
+
};
|
|
479
629
|
OperatorSessionRejectedError = class extends Error {
|
|
480
630
|
constructor(reason) {
|
|
481
631
|
super("The saved Operator Session was rejected by Gate.");
|
|
@@ -515,6 +665,112 @@ var init_gate_api = __esm({
|
|
|
515
665
|
}
|
|
516
666
|
});
|
|
517
667
|
|
|
668
|
+
// src/action-deadline.ts
|
|
669
|
+
function withinActionDeadline(operation, signal, deadlineMessage) {
|
|
670
|
+
return new Promise((resolve, reject) => {
|
|
671
|
+
const abort = () => reject(new Error(deadlineMessage));
|
|
672
|
+
if (signal.aborted) {
|
|
673
|
+
void operation.catch(() => void 0);
|
|
674
|
+
abort();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
678
|
+
operation.then(
|
|
679
|
+
(value) => {
|
|
680
|
+
signal.removeEventListener("abort", abort);
|
|
681
|
+
resolve(value);
|
|
682
|
+
},
|
|
683
|
+
(error) => {
|
|
684
|
+
signal.removeEventListener("abort", abort);
|
|
685
|
+
reject(error);
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
var init_action_deadline = __esm({
|
|
691
|
+
"src/action-deadline.ts"() {
|
|
692
|
+
"use strict";
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// src/zerodha-actions.ts
|
|
697
|
+
async function runZerodhaAction(api, action, operatorId, signal, attemptId, otp, options = {}) {
|
|
698
|
+
const started = Date.now();
|
|
699
|
+
let observedState;
|
|
700
|
+
let observedOperator2;
|
|
701
|
+
api.setActionSignal?.(signal);
|
|
702
|
+
try {
|
|
703
|
+
if (signal.aborted) throw new Error("Zerodha login observation exceeded its deadline.");
|
|
704
|
+
if (action === "status") {
|
|
705
|
+
const session = await withinActionDeadline(api.confirmZerodhaSession(), signal, "Zerodha login observation exceeded its deadline.");
|
|
706
|
+
if (session.operatorId && session.operatorId !== operatorId) throw new Error("Origin returned an unexpected Zerodha Session Operator.");
|
|
707
|
+
return { status: "succeeded", observedState: "present", ...session.operatorId ? { observedOperator: session.operatorId } : {}, elapsedMs: Date.now() - started };
|
|
708
|
+
}
|
|
709
|
+
if (action === "login") {
|
|
710
|
+
const attempt = await withinActionDeadline(api.startLogin(), signal, "Zerodha login observation exceeded its deadline.");
|
|
711
|
+
attemptId = attempt.attemptId;
|
|
712
|
+
if (attempt.operatorId !== operatorId) throw new Error("Zerodha login reached an unexpected Operator.");
|
|
713
|
+
}
|
|
714
|
+
if (!attemptId) throw new Error("Zerodha login requires an attempt identifier.");
|
|
715
|
+
if (action === "otp") {
|
|
716
|
+
if (!otp) throw new Error("Zerodha OTP input is required.");
|
|
717
|
+
await withinActionDeadline(api.submitOtp(attemptId, otp), signal, "Zerodha login observation exceeded its deadline.");
|
|
718
|
+
}
|
|
719
|
+
const observer = new AbortController();
|
|
720
|
+
const observationSignal = AbortSignal.any([signal, observer.signal]);
|
|
721
|
+
let result;
|
|
722
|
+
try {
|
|
723
|
+
await withinActionDeadline(api.watchLoginAttempt(attemptId, (event) => {
|
|
724
|
+
if (observer.signal.aborted || signal.aborted) return;
|
|
725
|
+
if (event.attemptId !== attemptId || event.type === "status" && event.operatorId !== operatorId) {
|
|
726
|
+
throw new Error("Origin returned an unexpected Zerodha login attempt.");
|
|
727
|
+
}
|
|
728
|
+
if (event.type === "status") {
|
|
729
|
+
observedState = event.status;
|
|
730
|
+
observedOperator2 = event.operatorId;
|
|
731
|
+
}
|
|
732
|
+
options.onObservation?.(event);
|
|
733
|
+
if (event.type === "status" && (event.status === "awaiting_otp" || event.status === "succeeded")) {
|
|
734
|
+
result = {
|
|
735
|
+
status: event.status,
|
|
736
|
+
attemptId,
|
|
737
|
+
observedOperator: observedOperator2,
|
|
738
|
+
observedState,
|
|
739
|
+
...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts },
|
|
740
|
+
...event.message ? { message: event.message } : {}
|
|
741
|
+
};
|
|
742
|
+
observer.abort();
|
|
743
|
+
}
|
|
744
|
+
}, observationSignal), observationSignal, "Zerodha login observation exceeded its deadline.");
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (!observer.signal.aborted || !result) throw error;
|
|
747
|
+
}
|
|
748
|
+
if (signal.aborted || !result) throw new Error("Zerodha login observation exceeded its deadline.");
|
|
749
|
+
if (result.status === "succeeded") {
|
|
750
|
+
const session = await withinActionDeadline(api.confirmZerodhaSession(), signal, "Zerodha login observation exceeded its deadline.");
|
|
751
|
+
if (session.operatorId && session.operatorId !== operatorId) throw new Error("Origin returned an unexpected Zerodha Session Operator.");
|
|
752
|
+
}
|
|
753
|
+
return { ...result, elapsedMs: Date.now() - started };
|
|
754
|
+
} catch (cause) {
|
|
755
|
+
const error = cause instanceof Error ? cause : new Error("Zerodha action failed.");
|
|
756
|
+
Object.assign(error, {
|
|
757
|
+
...attemptId ? { attemptId, recovery: "Observe this exact attempt with the same target and profile; start explicitly only if Gate reports it inactive." } : {},
|
|
758
|
+
...observedState ? { observedState } : {},
|
|
759
|
+
...observedOperator2 ? { observedOperator: observedOperator2 } : {},
|
|
760
|
+
elapsedMs: Date.now() - started
|
|
761
|
+
});
|
|
762
|
+
throw error;
|
|
763
|
+
} finally {
|
|
764
|
+
api.setActionSignal?.();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
var init_zerodha_actions = __esm({
|
|
768
|
+
"src/zerodha-actions.ts"() {
|
|
769
|
+
"use strict";
|
|
770
|
+
init_action_deadline();
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
518
774
|
// src/credential-app.tsx
|
|
519
775
|
var credential_app_exports = {};
|
|
520
776
|
__export(credential_app_exports, {
|
|
@@ -581,6 +837,24 @@ __export(app_exports, {
|
|
|
581
837
|
import { useEffect, useRef, useState as useState2 } from "react";
|
|
582
838
|
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
583
839
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
840
|
+
function formatZerodhaValidationReport(report) {
|
|
841
|
+
return [
|
|
842
|
+
"Scenario: zerodha-session",
|
|
843
|
+
`Outcome: ${report.outcome}`,
|
|
844
|
+
`Reason: ${report.reason}`,
|
|
845
|
+
`Target: ${report.target.environment} ${report.target.origin}`,
|
|
846
|
+
`Profile: ${report.profile}`,
|
|
847
|
+
`Observed Operator: ${report.observedOperator ?? "not observed"}`,
|
|
848
|
+
`Observed state: ${report.observedState ?? "not observed"}`,
|
|
849
|
+
`Attempt: ${report.attemptId ?? "not observed"}`,
|
|
850
|
+
`Elapsed: ${report.elapsedMs} ms`,
|
|
851
|
+
`Request ID: ${report.requestId ?? "unavailable"}`,
|
|
852
|
+
`Build ID: ${report.buildId ?? "unavailable"}`,
|
|
853
|
+
...report.failedOtpAttempts === void 0 ? [] : [`Failed OTP attempts: ${report.failedOtpAttempts}`],
|
|
854
|
+
...report.message ? [report.message] : [],
|
|
855
|
+
...report.nextStep ? [`Next step: ${report.nextStep}`] : []
|
|
856
|
+
].join("\n");
|
|
857
|
+
}
|
|
584
858
|
function formatValidationReport(report) {
|
|
585
859
|
const lines = [
|
|
586
860
|
`Outcome: ${report.outcome}`,
|
|
@@ -603,14 +877,18 @@ function formatValidationReport(report) {
|
|
|
603
877
|
}
|
|
604
878
|
return lines.join("\n");
|
|
605
879
|
}
|
|
606
|
-
function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperatorActivity, onStatus, onValidate, onLogout }) {
|
|
880
|
+
function LoginApp({ api, expectedOperatorId = "OPERATOR_1", actionTimeoutMs = 3e4, onResumeSession, onReportOperatorActivity, onStatus, onValidate, onValidateZerodha, onLogout }) {
|
|
607
881
|
const [step, setStep] = useState2(onResumeSession ? "checking" : "username");
|
|
608
882
|
const [username, setUsername] = useState2("");
|
|
609
883
|
const [password, setPassword] = useState2("");
|
|
610
884
|
const [otp, setOtp] = useState2("");
|
|
885
|
+
const [progress, setProgress] = useState2(null);
|
|
611
886
|
const [error, setError] = useState2("");
|
|
612
887
|
const [menuMessage, setMenuMessage] = useState2("");
|
|
613
888
|
const [validationReport, setValidationReport] = useState2(null);
|
|
889
|
+
const [zerodhaValidationReport, setZerodhaValidationReport] = useState2(null);
|
|
890
|
+
const [resumeAttemptInput, setResumeAttemptInput] = useState2("");
|
|
891
|
+
const validatingZerodha = useRef(false);
|
|
614
892
|
const [selectedMenuIndex, setSelectedMenuIndex] = useState2(0);
|
|
615
893
|
const attemptId = useRef(null);
|
|
616
894
|
const watcher = useRef(null);
|
|
@@ -695,80 +973,105 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
695
973
|
setStep("username");
|
|
696
974
|
}
|
|
697
975
|
}
|
|
698
|
-
function applyLoginEvent(event, signal) {
|
|
699
|
-
if (signal.aborted) return;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
976
|
+
function applyLoginEvent(event, signal, action) {
|
|
977
|
+
if (signal.aborted || event.type !== "status") return;
|
|
978
|
+
attemptId.current = event.attemptId;
|
|
979
|
+
setProgress({
|
|
980
|
+
status: event.status === "succeeded" ? "succeeded" : "awaiting_otp",
|
|
981
|
+
attemptId: event.attemptId,
|
|
982
|
+
observedState: event.status,
|
|
983
|
+
observedOperator: event.operatorId,
|
|
984
|
+
...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts }
|
|
985
|
+
});
|
|
986
|
+
if (event.status === "loading") setStep(action === "login" ? "waiting" : "verifying");
|
|
987
|
+
}
|
|
988
|
+
async function runLoginAction(action, controller, code) {
|
|
989
|
+
try {
|
|
990
|
+
const result = await runZerodhaAction(
|
|
991
|
+
api,
|
|
992
|
+
action,
|
|
993
|
+
expectedOperatorId,
|
|
994
|
+
controller.signal,
|
|
995
|
+
action === "otp" ? attemptId.current ?? void 0 : void 0,
|
|
996
|
+
code,
|
|
997
|
+
{ onObservation: (event) => applyLoginEvent(event, controller.signal, action) }
|
|
998
|
+
);
|
|
999
|
+
if (controller.signal.aborted) return;
|
|
1000
|
+
attemptId.current = result.attemptId ?? null;
|
|
1001
|
+
setProgress(result);
|
|
703
1002
|
setOtp("");
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1003
|
+
if (result.status === "awaiting_otp") {
|
|
1004
|
+
setError(result.message ?? "");
|
|
1005
|
+
setStep("otp");
|
|
1006
|
+
} else {
|
|
1007
|
+
clearZerodhaLoginDeadline(controller);
|
|
1008
|
+
setStep("success");
|
|
1009
|
+
}
|
|
1010
|
+
} catch (cause) {
|
|
1011
|
+
if (controller.signal.aborted) return;
|
|
1012
|
+
clearZerodhaLoginDeadline(controller);
|
|
1013
|
+
setOtp("");
|
|
1014
|
+
const recovery = cause;
|
|
1015
|
+
if (recovery?.attemptId) attemptId.current = recovery.attemptId;
|
|
1016
|
+
setError(`${cause instanceof Error ? cause.message : "Login with Zerodha failed."}${recovery?.attemptId ? ` Attempt: ${recovery.attemptId}. ${recovery.recovery ?? "Observe this exact attempt with the same target and profile."}` : ""}`);
|
|
1017
|
+
setStep(cause instanceof OperatorSessionRejectedError ? "username" : "menu");
|
|
709
1018
|
}
|
|
710
1019
|
}
|
|
711
1020
|
async function beginLogin() {
|
|
1021
|
+
validatingZerodha.current = false;
|
|
1022
|
+
setZerodhaValidationReport(null);
|
|
712
1023
|
setError("");
|
|
1024
|
+
setProgress(null);
|
|
1025
|
+
attemptId.current = null;
|
|
713
1026
|
setStep("starting");
|
|
714
1027
|
const controller = new AbortController();
|
|
715
1028
|
watcher.current?.abort();
|
|
716
1029
|
watcher.current = controller;
|
|
717
|
-
api.setActionSignal?.(controller.signal);
|
|
718
1030
|
const timer = setTimeout(() => {
|
|
719
1031
|
if (zerodhaLoginDeadline.current?.controller !== controller) return;
|
|
720
1032
|
controller.abort();
|
|
721
1033
|
clearZerodhaLoginDeadline(controller);
|
|
722
|
-
|
|
1034
|
+
setOtp("");
|
|
1035
|
+
setError(`Login with Zerodha exceeded its deadline.${attemptId.current ? ` Observe attempt ${attemptId.current} with the same target and profile.` : ""}`);
|
|
723
1036
|
setStep("menu");
|
|
724
1037
|
}, actionTimeoutMs);
|
|
725
1038
|
zerodhaLoginDeadline.current = { controller, timer };
|
|
726
|
-
|
|
727
|
-
const login = await api.startLogin();
|
|
728
|
-
if (controller.signal.aborted) return;
|
|
729
|
-
attemptId.current = login.attemptId;
|
|
730
|
-
setStep("waiting");
|
|
731
|
-
void api.watchLoginAttempt(login.attemptId, (event) => applyLoginEvent(event, controller.signal), controller.signal).then(async () => {
|
|
732
|
-
if (controller.signal.aborted) return;
|
|
733
|
-
await api.confirmZerodhaSession();
|
|
734
|
-
if (!controller.signal.aborted) {
|
|
735
|
-
clearZerodhaLoginDeadline(controller);
|
|
736
|
-
setStep("success");
|
|
737
|
-
}
|
|
738
|
-
}).catch((cause) => {
|
|
739
|
-
if (controller.signal.aborted) return;
|
|
740
|
-
clearZerodhaLoginDeadline(controller);
|
|
741
|
-
setError(cause instanceof Error ? cause.message : "Login with Zerodha failed.");
|
|
742
|
-
setStep("menu");
|
|
743
|
-
});
|
|
744
|
-
} catch (cause) {
|
|
745
|
-
if (controller.signal.aborted) return;
|
|
746
|
-
clearZerodhaLoginDeadline(controller);
|
|
747
|
-
setError(cause instanceof Error ? cause.message : "Could not start Login with Zerodha.");
|
|
748
|
-
setStep("menu");
|
|
749
|
-
}
|
|
1039
|
+
await runLoginAction("login", controller);
|
|
750
1040
|
}
|
|
751
1041
|
async function submitOtp() {
|
|
752
1042
|
if (!/^\d{6}$/.test(otp)) {
|
|
753
1043
|
setError("Enter the 6-digit Zerodha OTP.");
|
|
754
1044
|
return;
|
|
755
1045
|
}
|
|
756
|
-
if (!attemptId.current) {
|
|
1046
|
+
if (!attemptId.current || !watcher.current || watcher.current.signal.aborted) {
|
|
1047
|
+
setOtp("");
|
|
757
1048
|
setError("The Zerodha login attempt is no longer active.");
|
|
758
1049
|
setStep("menu");
|
|
759
1050
|
return;
|
|
760
1051
|
}
|
|
1052
|
+
const code = otp;
|
|
1053
|
+
setOtp("");
|
|
761
1054
|
setError("");
|
|
762
1055
|
setStep("verifying");
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1056
|
+
if (validatingZerodha.current) await runValidation(watcher.current, attemptId.current, code);
|
|
1057
|
+
else await runLoginAction("otp", watcher.current, code);
|
|
1058
|
+
}
|
|
1059
|
+
function checkZerodhaSession() {
|
|
1060
|
+
setError("");
|
|
1061
|
+
setMenuMessage("");
|
|
1062
|
+
setStep("checking");
|
|
1063
|
+
const controller = new AbortController();
|
|
1064
|
+
watcher.current = controller;
|
|
1065
|
+
const timer = setTimeout(() => controller.abort(), actionTimeoutMs);
|
|
1066
|
+
void runZerodhaAction(api, "status", expectedOperatorId, controller.signal).then(() => {
|
|
1067
|
+
setMenuMessage("Zerodha Session confirmed by Gate.");
|
|
1068
|
+
setStep("menu");
|
|
1069
|
+
}).catch((cause) => {
|
|
1070
|
+
setError(cause instanceof Error ? cause.message : "Could not check the Zerodha Session.");
|
|
1071
|
+
setStep(cause instanceof OperatorSessionRejectedError ? "username" : "menu");
|
|
1072
|
+
}).finally(() => {
|
|
1073
|
+
clearTimeout(timer);
|
|
1074
|
+
});
|
|
772
1075
|
}
|
|
773
1076
|
function checkOperatorSession() {
|
|
774
1077
|
if (!onStatus) return;
|
|
@@ -805,6 +1108,81 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
805
1108
|
setStep("menu");
|
|
806
1109
|
});
|
|
807
1110
|
}
|
|
1111
|
+
async function runValidation(controller, selectedAttempt, code) {
|
|
1112
|
+
if (!onValidateZerodha) return;
|
|
1113
|
+
try {
|
|
1114
|
+
const result = await onValidateZerodha({
|
|
1115
|
+
attemptId: selectedAttempt,
|
|
1116
|
+
otp: code,
|
|
1117
|
+
signal: controller.signal,
|
|
1118
|
+
onObservation: (event) => applyLoginEvent(event, controller.signal, code ? "otp" : "login")
|
|
1119
|
+
});
|
|
1120
|
+
if (watcher.current !== controller) return;
|
|
1121
|
+
setOtp("");
|
|
1122
|
+
setZerodhaValidationReport(result);
|
|
1123
|
+
attemptId.current = result.attemptId ?? null;
|
|
1124
|
+
if (!controller.signal.aborted && result.attemptId && ["awaiting_otp", "otp_rejected"].includes(result.reason)) {
|
|
1125
|
+
setStep("otp");
|
|
1126
|
+
} else {
|
|
1127
|
+
clearZerodhaLoginDeadline(controller);
|
|
1128
|
+
validatingZerodha.current = false;
|
|
1129
|
+
if (result.reason === "operator_login_required") {
|
|
1130
|
+
setUsername("");
|
|
1131
|
+
setPassword("");
|
|
1132
|
+
}
|
|
1133
|
+
setStep(result.reason === "operator_login_required" ? "username" : "menu");
|
|
1134
|
+
}
|
|
1135
|
+
} catch (cause) {
|
|
1136
|
+
if (watcher.current !== controller) return;
|
|
1137
|
+
clearZerodhaLoginDeadline(controller);
|
|
1138
|
+
validatingZerodha.current = false;
|
|
1139
|
+
setOtp("");
|
|
1140
|
+
const message = cause instanceof Error ? cause.message : "Zerodha Session validation failed.";
|
|
1141
|
+
setError(code ? message.replaceAll(code, "[REDACTED]") : message);
|
|
1142
|
+
setStep("menu");
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async function beginValidation(selectedAttempt) {
|
|
1146
|
+
watcher.current?.abort();
|
|
1147
|
+
clearZerodhaLoginDeadline();
|
|
1148
|
+
const controller = new AbortController();
|
|
1149
|
+
const started = Date.now();
|
|
1150
|
+
watcher.current = controller;
|
|
1151
|
+
validatingZerodha.current = true;
|
|
1152
|
+
attemptId.current = selectedAttempt ?? null;
|
|
1153
|
+
setOtp("");
|
|
1154
|
+
setProgress(null);
|
|
1155
|
+
setError("");
|
|
1156
|
+
setMenuMessage("");
|
|
1157
|
+
setValidationReport(null);
|
|
1158
|
+
setZerodhaValidationReport(null);
|
|
1159
|
+
setStep("validatingZerodha");
|
|
1160
|
+
const timer = setTimeout(() => {
|
|
1161
|
+
if (zerodhaLoginDeadline.current?.controller !== controller) return;
|
|
1162
|
+
controller.abort();
|
|
1163
|
+
clearZerodhaLoginDeadline(controller);
|
|
1164
|
+
validatingZerodha.current = false;
|
|
1165
|
+
setOtp("");
|
|
1166
|
+
setZerodhaValidationReport((previous) => previous ? {
|
|
1167
|
+
...previous,
|
|
1168
|
+
outcome: "fail",
|
|
1169
|
+
reason: "deadline_exceeded",
|
|
1170
|
+
exitCode: 1,
|
|
1171
|
+
message: "The Zerodha Session validation exceeded its deadline.",
|
|
1172
|
+
elapsedMs: Date.now() - started,
|
|
1173
|
+
nextStep: `Observe attempt ${previous.attemptId} with the same target and profile; explicitly start again only if Gate reports it inactive.`
|
|
1174
|
+
} : previous);
|
|
1175
|
+
setError(`Zerodha Session validation exceeded its deadline.${attemptId.current ? ` Observe attempt ${attemptId.current} with the same target and profile.` : ""}`);
|
|
1176
|
+
setStep("menu");
|
|
1177
|
+
}, actionTimeoutMs);
|
|
1178
|
+
zerodhaLoginDeadline.current = { controller, timer };
|
|
1179
|
+
await runValidation(controller, selectedAttempt);
|
|
1180
|
+
}
|
|
1181
|
+
function resumeValidation() {
|
|
1182
|
+
setError("");
|
|
1183
|
+
setResumeAttemptInput("");
|
|
1184
|
+
setStep("validationAttempt");
|
|
1185
|
+
}
|
|
808
1186
|
function logoutOperatorSession() {
|
|
809
1187
|
if (!onLogout) return;
|
|
810
1188
|
setError("");
|
|
@@ -814,6 +1192,7 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
814
1192
|
setPassword("");
|
|
815
1193
|
setMenuMessage("");
|
|
816
1194
|
setValidationReport(null);
|
|
1195
|
+
setZerodhaValidationReport(null);
|
|
817
1196
|
setStep("username");
|
|
818
1197
|
}).catch((cause) => {
|
|
819
1198
|
setError(cause instanceof Error ? cause.message : "Could not log out the Operator Session.");
|
|
@@ -826,7 +1205,14 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
826
1205
|
} },
|
|
827
1206
|
...onStatus ? [{ shortcut: "2", label: "Check Operator Session", run: checkOperatorSession }] : [],
|
|
828
1207
|
...onValidate ? [{ shortcut: "3", label: "Validate Operator Session", run: validateOperatorSession }] : [],
|
|
829
|
-
...onLogout ? [{ shortcut: "4", label: "Logout Operator Session", run: logoutOperatorSession }] : []
|
|
1208
|
+
...onLogout ? [{ shortcut: "4", label: "Logout Operator Session", run: logoutOperatorSession }] : [],
|
|
1209
|
+
{ shortcut: "5", label: "Check Zerodha Session", run: checkZerodhaSession },
|
|
1210
|
+
...onValidateZerodha ? [
|
|
1211
|
+
{ shortcut: "6", label: "Validate Zerodha Session", run: () => {
|
|
1212
|
+
void beginValidation();
|
|
1213
|
+
} },
|
|
1214
|
+
{ shortcut: "7", label: "Resume Zerodha Validation", run: resumeValidation }
|
|
1215
|
+
] : []
|
|
830
1216
|
];
|
|
831
1217
|
const activeMenuIndex = Math.min(selectedMenuIndex, menuActions.length - 1);
|
|
832
1218
|
useInput2((input, key) => {
|
|
@@ -864,6 +1250,15 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
864
1250
|
}
|
|
865
1251
|
return;
|
|
866
1252
|
}
|
|
1253
|
+
if (step === "validationAttempt") {
|
|
1254
|
+
if (key.escape) setStep("menu");
|
|
1255
|
+
else if (key.return) {
|
|
1256
|
+
if (!/^[a-zA-Z0-9_-]{1,200}$/.test(resumeAttemptInput)) setError("Enter an explicit valid Zerodha attempt ID.");
|
|
1257
|
+
else void beginValidation(resumeAttemptInput);
|
|
1258
|
+
} else if (key.backspace || key.delete) setResumeAttemptInput((value) => value.slice(0, -1));
|
|
1259
|
+
else if (input && !key.ctrl && !key.meta) setResumeAttemptInput((value) => (value + input).slice(0, 200));
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
867
1262
|
if (step === "menu") {
|
|
868
1263
|
if (key.upArrow) {
|
|
869
1264
|
setSelectedMenuIndex((index) => (index - 1 + menuActions.length) % menuActions.length);
|
|
@@ -878,7 +1273,13 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
878
1273
|
return;
|
|
879
1274
|
}
|
|
880
1275
|
if (step === "otp") {
|
|
881
|
-
if (key.
|
|
1276
|
+
if (key.escape && validatingZerodha.current) {
|
|
1277
|
+
watcher.current?.abort();
|
|
1278
|
+
clearZerodhaLoginDeadline();
|
|
1279
|
+
validatingZerodha.current = false;
|
|
1280
|
+
setOtp("");
|
|
1281
|
+
setStep("menu");
|
|
1282
|
+
} else if (key.return) void submitOtp();
|
|
882
1283
|
else if (key.backspace || key.delete) setOtp((value) => value.slice(0, -1));
|
|
883
1284
|
else if (input) setOtp((value) => (value + input.replace(/\D/g, "")).slice(0, 6));
|
|
884
1285
|
}
|
|
@@ -923,6 +1324,16 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
923
1324
|
] }),
|
|
924
1325
|
step === "checking" && /* @__PURE__ */ jsx2(Text2, { children: "Checking Operator Session\u2026" }),
|
|
925
1326
|
step === "validating" && /* @__PURE__ */ jsx2(Text2, { children: "Validating Operator Session\u2026" }),
|
|
1327
|
+
step === "validatingZerodha" && /* @__PURE__ */ jsx2(Text2, { children: "Validating Zerodha Session\u2026" }),
|
|
1328
|
+
step === "validationAttempt" && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1329
|
+
/* @__PURE__ */ jsx2(Text2, { children: "Zerodha attempt ID" }),
|
|
1330
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1331
|
+
resumeAttemptInput,
|
|
1332
|
+
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " })
|
|
1333
|
+
] }),
|
|
1334
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Enter to observe this attempt \xB7 Esc to return" })
|
|
1335
|
+
] }),
|
|
1336
|
+
zerodhaValidationReport ? /* @__PURE__ */ jsx2(Text2, { children: formatZerodhaValidationReport(zerodhaValidationReport) }) : null,
|
|
926
1337
|
step === "loggingOut" && /* @__PURE__ */ jsx2(Text2, { children: "Logging out of the Operator Session\u2026" }),
|
|
927
1338
|
step === "starting" && /* @__PURE__ */ jsx2(Text2, { children: "Starting Login with Zerodha\u2026" }),
|
|
928
1339
|
step === "waiting" && /* @__PURE__ */ jsx2(Text2, { children: "Connecting to Zerodha\u2026" }),
|
|
@@ -931,9 +1342,22 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
931
1342
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
932
1343
|
"\u2022".repeat(otp.length),
|
|
933
1344
|
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " })
|
|
934
|
-
] })
|
|
1345
|
+
] }),
|
|
1346
|
+
validatingZerodha.current ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Enter to submit \xB7 Esc to return and resume later" }) : null
|
|
935
1347
|
] }),
|
|
936
1348
|
step === "verifying" && /* @__PURE__ */ jsx2(Text2, { children: "Verifying Zerodha login\u2026" }),
|
|
1349
|
+
progress?.attemptId ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1350
|
+
"Attempt: ",
|
|
1351
|
+
progress.attemptId
|
|
1352
|
+
] }) : null,
|
|
1353
|
+
progress?.observedState ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1354
|
+
"Zerodha state: ",
|
|
1355
|
+
progress.observedState
|
|
1356
|
+
] }) : null,
|
|
1357
|
+
progress?.failedOtpAttempts !== void 0 ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1358
|
+
"Failed OTP attempts: ",
|
|
1359
|
+
progress.failedOtpAttempts
|
|
1360
|
+
] }) : null,
|
|
937
1361
|
error ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }) : null
|
|
938
1362
|
] });
|
|
939
1363
|
}
|
|
@@ -941,6 +1365,7 @@ var init_app = __esm({
|
|
|
941
1365
|
"src/app.tsx"() {
|
|
942
1366
|
"use strict";
|
|
943
1367
|
init_gate_api();
|
|
1368
|
+
init_zerodha_actions();
|
|
944
1369
|
}
|
|
945
1370
|
});
|
|
946
1371
|
|
|
@@ -1492,9 +1917,9 @@ function sessionHasCookies(session) {
|
|
|
1492
1917
|
return Boolean(session.operatorSessionCookie || session.clientIdentityCookie);
|
|
1493
1918
|
}
|
|
1494
1919
|
var CliProfileStore = class {
|
|
1495
|
-
constructor(homeDirectory = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd(), lockWaitMs = LOCK_WAIT_MS) {
|
|
1920
|
+
constructor(homeDirectory = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd(), lockWaitMs = LOCK_WAIT_MS, directoryName = ".lmt") {
|
|
1496
1921
|
this.lockWaitMs = lockWaitMs;
|
|
1497
|
-
this.directory = path2.join(homeDirectory,
|
|
1922
|
+
this.directory = path2.join(homeDirectory, directoryName);
|
|
1498
1923
|
this.filePath = path2.join(this.directory, FILE_NAME);
|
|
1499
1924
|
this.lockPath = path2.join(this.directory, `${FILE_NAME}.lock`);
|
|
1500
1925
|
}
|
|
@@ -1722,45 +2147,161 @@ var CliProfileStore = class {
|
|
|
1722
2147
|
}
|
|
1723
2148
|
};
|
|
1724
2149
|
|
|
1725
|
-
// src/zerodha-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
2150
|
+
// src/zerodha-validation.ts
|
|
2151
|
+
init_action_deadline();
|
|
2152
|
+
init_gate_api();
|
|
2153
|
+
init_profile_name();
|
|
2154
|
+
|
|
2155
|
+
// src/protected-input.ts
|
|
2156
|
+
var ProtectedInputError = class extends Error {
|
|
2157
|
+
code = "invalid_invocation";
|
|
2158
|
+
};
|
|
2159
|
+
async function readProtectedOtp(stdin, signal) {
|
|
2160
|
+
if (stdin.isTTY) throw new ProtectedInputError("OTP requires piped protected stdin; interactive mode provides masked entry.");
|
|
2161
|
+
const chunks = [];
|
|
2162
|
+
let size = 0;
|
|
2163
|
+
try {
|
|
2164
|
+
const iterator = stdin[Symbol.asyncIterator]();
|
|
2165
|
+
while (true) {
|
|
2166
|
+
if (signal.aborted) throw new Error("The action exceeded its deadline.");
|
|
2167
|
+
const next = await new Promise((resolve, reject) => {
|
|
2168
|
+
const abort = () => {
|
|
2169
|
+
stdin.destroy?.();
|
|
2170
|
+
reject(new Error("The action exceeded its deadline."));
|
|
2171
|
+
};
|
|
2172
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
2173
|
+
Promise.resolve(iterator.next()).then(resolve, () => reject(new ProtectedInputError("Could not read OTP from protected stdin; provide one six-digit line."))).finally(() => signal.removeEventListener("abort", abort));
|
|
2174
|
+
});
|
|
2175
|
+
if (next.done) break;
|
|
2176
|
+
size += typeof next.value === "string" ? Buffer.byteLength(next.value) : next.value.byteLength;
|
|
2177
|
+
if (size > 8) {
|
|
2178
|
+
stdin.destroy?.();
|
|
2179
|
+
throw new ProtectedInputError("OTP input is too large; provide one six-digit line.");
|
|
2180
|
+
}
|
|
2181
|
+
chunks.push(Buffer.from(next.value));
|
|
2182
|
+
}
|
|
2183
|
+
const buffer = Buffer.concat(chunks);
|
|
2184
|
+
try {
|
|
2185
|
+
const value = buffer.toString("utf8");
|
|
2186
|
+
if (!/^\d{6}(?:\r?\n)?$/.test(value)) throw new ProtectedInputError("OTP input must be one six-digit line.");
|
|
2187
|
+
return value.slice(0, 6);
|
|
2188
|
+
} finally {
|
|
2189
|
+
buffer.fill(0);
|
|
1756
2190
|
}
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
if (signal.aborted || error instanceof ProtectedInputError) throw error;
|
|
2193
|
+
throw new ProtectedInputError("Could not read OTP from protected stdin; provide one six-digit line.");
|
|
2194
|
+
} finally {
|
|
2195
|
+
for (const chunk of chunks) chunk.fill(0);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/zerodha-validation.ts
|
|
2200
|
+
init_zerodha_actions();
|
|
2201
|
+
async function runZerodhaValidation(options) {
|
|
2202
|
+
const started = performance.now();
|
|
2203
|
+
const controller = new AbortController();
|
|
2204
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 12e4);
|
|
2205
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
2206
|
+
const observations = [];
|
|
2207
|
+
let api;
|
|
2208
|
+
let otp;
|
|
2209
|
+
let result = {
|
|
2210
|
+
scenario: "zerodha-session",
|
|
2211
|
+
outcome: "fail",
|
|
2212
|
+
reason: "validation_action_failed",
|
|
2213
|
+
exitCode: 1,
|
|
2214
|
+
target: { environment: options.target.environment, origin: options.target.origin },
|
|
2215
|
+
profile: options.callerProfile,
|
|
2216
|
+
...options.attemptId ? { attemptId: options.attemptId } : {},
|
|
2217
|
+
observations,
|
|
2218
|
+
nextStep: null
|
|
2219
|
+
};
|
|
2220
|
+
const safeText = (message) => (otp ? message.replaceAll(otp, "[REDACTED]") : message).replace(/lmt_(?:operator_session|client_id)=[^\s,;]+/g, "[REDACTED]").replace(/(password|token|credential|cookie)\s*[=:]\s*[^\s,;]+/gi, "$1=[REDACTED]");
|
|
2221
|
+
const resumeStep = () => result.attemptId ? `Use the same environment and profile: validate zerodha-session --attempt ${result.attemptId} --otp-stdin. Observe without --otp-stdin; start explicitly only if Gate reports the attempt inactive.` : "Run validate zerodha-session again after resolving the reported prerequisite.";
|
|
2222
|
+
try {
|
|
2223
|
+
if (!["local", "dev"].includes(options.target.environment)) throw Object.assign(new Error("The CLI supports local or dev targets only; prod is not permitted."), { code: "invalid_target" });
|
|
2224
|
+
try {
|
|
2225
|
+
assertClientProfileName(options.callerProfile);
|
|
2226
|
+
} catch {
|
|
2227
|
+
throw new ProtectedInputError(CLIENT_PROFILE_NAME_ERROR);
|
|
2228
|
+
}
|
|
2229
|
+
if (options.attemptId !== void 0 && !/^[a-zA-Z0-9_-]{1,200}$/.test(options.attemptId)) throw new ProtectedInputError("--attempt must be a valid Zerodha attempt ID.");
|
|
2230
|
+
if (options.readOtp && !options.attemptId) throw new ProtectedInputError("OTP submission requires an explicit --attempt ID from a prior login or validation.");
|
|
2231
|
+
const actions = createOperatorSessionActions({
|
|
2232
|
+
target: options.target,
|
|
2233
|
+
profile: options.callerProfile,
|
|
2234
|
+
store: options.store,
|
|
2235
|
+
timeoutMs: options.timeoutMs ?? 12e4,
|
|
2236
|
+
signal,
|
|
2237
|
+
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
2238
|
+
});
|
|
2239
|
+
api = await withinActionDeadline(actions.getApi(), signal, "The Zerodha Session validation exceeded its deadline.");
|
|
2240
|
+
const operator = await actions.status();
|
|
2241
|
+
result.observedOperator = operator.observedOperator;
|
|
2242
|
+
if (options.readOtp) {
|
|
2243
|
+
otp = await withinActionDeadline(options.readOtp(signal), signal, "The Zerodha Session validation exceeded its deadline.");
|
|
2244
|
+
if (!/^\d{6}$/.test(otp)) throw new ProtectedInputError("OTP input must be one six-digit line.");
|
|
2245
|
+
}
|
|
2246
|
+
const action = options.attemptId ? options.readOtp ? "otp" : "observe" : "login";
|
|
2247
|
+
const login = await runZerodhaAction(api, action, options.target.operatorId, signal, options.attemptId, otp, {
|
|
2248
|
+
onObservation(event) {
|
|
2249
|
+
result.attemptId = event.attemptId;
|
|
2250
|
+
if (event.type === "status") {
|
|
2251
|
+
result.observedState = event.status;
|
|
2252
|
+
result.observedOperator = event.operatorId;
|
|
2253
|
+
observations.push(event.status);
|
|
2254
|
+
if (event.failedOtpAttempts !== void 0) result.failedOtpAttempts = event.failedOtpAttempts;
|
|
2255
|
+
if (event.message) result.message = safeText(event.message);
|
|
2256
|
+
}
|
|
2257
|
+
options.onObservation?.(event);
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
result.attemptId = login.attemptId;
|
|
2261
|
+
if (login.status === "awaiting_otp") {
|
|
2262
|
+
const rejected = !!options.readOtp && (login.failedOtpAttempts ?? 0) > 0;
|
|
2263
|
+
result = { ...result, outcome: rejected ? "fail" : "blocked", reason: rejected ? "otp_rejected" : "awaiting_otp", exitCode: rejected ? 1 : 4, nextStep: resumeStep() };
|
|
2264
|
+
} else {
|
|
2265
|
+
result = { ...result, outcome: "pass", reason: "zerodha_session_validated", exitCode: 0, nextStep: null };
|
|
2266
|
+
}
|
|
2267
|
+
} catch (cause) {
|
|
2268
|
+
const error = cause instanceof Error ? cause : void 0;
|
|
2269
|
+
if (error?.attemptId) result.attemptId = error.attemptId;
|
|
2270
|
+
if (error?.observedState) result.observedState = error.observedState;
|
|
2271
|
+
if (error?.observedOperator) result.observedOperator = error.observedOperator;
|
|
2272
|
+
const code = error?.code;
|
|
2273
|
+
if (signal.aborted) {
|
|
2274
|
+
result.reason = "deadline_exceeded";
|
|
2275
|
+
result.message = "The Zerodha Session validation exceeded its deadline.";
|
|
2276
|
+
} else if (cause instanceof OperatorSessionRejectedError) {
|
|
2277
|
+
result.outcome = "blocked";
|
|
2278
|
+
result.reason = "operator_login_required";
|
|
2279
|
+
result.exitCode = 3;
|
|
2280
|
+
result.message = "No valid Operator Session is available; run login with this environment and profile first.";
|
|
2281
|
+
} else if (code && ["zerodha_credentials_missing", "secret_store_unavailable", "operator_not_configured"].includes(code)) {
|
|
2282
|
+
result.outcome = "blocked";
|
|
2283
|
+
result.reason = code;
|
|
2284
|
+
result.exitCode = 4;
|
|
2285
|
+
result.message = safeText(error.message);
|
|
2286
|
+
} else {
|
|
2287
|
+
result.reason = cause instanceof ProtectedInputError ? "invalid_invocation" : cause instanceof ZerodhaLoginAttemptUnavailableError ? "login_attempt_inactive" : cause instanceof ZerodhaLoginFailedError ? "zerodha_login_failed" : code ?? "validation_action_failed";
|
|
2288
|
+
result.exitCode = cause instanceof ProtectedInputError || code === "invalid_target" ? 2 : 1;
|
|
2289
|
+
result.message = error ? safeText(error.message) : "The Zerodha Session validation failed.";
|
|
2290
|
+
}
|
|
2291
|
+
result.nextStep = result.reason === "operator_login_required" ? "Run login with the selected environment and profile, then explicitly start Zerodha validation." : result.reason === "zerodha_credentials_missing" ? "Provision the selected Operator's Broker-owned Zerodha credentials, then explicitly start validation." : result.reason === "secret_store_unavailable" ? "Restore Broker access to its secret store, then explicitly start validation." : result.reason === "operator_not_configured" ? "Configure the selected Operator on Broker, then explicitly start validation." : resumeStep();
|
|
2292
|
+
if (cause instanceof OperatorSessionRejectedError && api && !signal.aborted) {
|
|
2293
|
+
await options.store.saveSession(options.callerProfile, options.target, api.getCookies()).catch(() => void 0);
|
|
2294
|
+
}
|
|
2295
|
+
} finally {
|
|
2296
|
+
otp = void 0;
|
|
2297
|
+
clearTimeout(timer);
|
|
2298
|
+
}
|
|
2299
|
+
const metadata = api?.responseMetadata();
|
|
2300
|
+
return { ...result, elapsedMs: Math.round(performance.now() - started), requestId: metadata?.requestId ?? null, buildId: metadata?.buildId ?? null };
|
|
1761
2301
|
}
|
|
1762
2302
|
|
|
1763
2303
|
// src/commands.ts
|
|
2304
|
+
init_zerodha_actions();
|
|
1764
2305
|
init_gate_api();
|
|
1765
2306
|
|
|
1766
2307
|
// src/validation-scenario.ts
|
|
@@ -2025,17 +2566,19 @@ function parseTimeout(raw) {
|
|
|
2025
2566
|
return Math.ceil(value);
|
|
2026
2567
|
}
|
|
2027
2568
|
function splitHeadlessArgv(argv) {
|
|
2569
|
+
if (hasArgumentOtp(argv)) throw new InvocationError("--otp arguments are no longer accepted. Use --otp-stdin with an explicit --attempt ID.");
|
|
2028
2570
|
if (argv.length === 0 || argv[0] === "interactive") {
|
|
2029
2571
|
throw new InvocationError("Choose a headless command or run `live-market-terminal interactive`.");
|
|
2030
2572
|
}
|
|
2031
2573
|
const options = /* @__PURE__ */ new Map();
|
|
2032
2574
|
let credentialStdin = false;
|
|
2575
|
+
let otpStdin = false;
|
|
2033
2576
|
const positional = [];
|
|
2034
2577
|
for (let index = 0; index < argv.length; index += 1) {
|
|
2035
2578
|
const token = argv[index];
|
|
2036
|
-
if (token
|
|
2037
|
-
if (
|
|
2038
|
-
|
|
2579
|
+
if (token === "--otp-stdin") {
|
|
2580
|
+
if (otpStdin) throw new InvocationError("Argument --otp-stdin may be specified only once.");
|
|
2581
|
+
otpStdin = true;
|
|
2039
2582
|
continue;
|
|
2040
2583
|
}
|
|
2041
2584
|
if (token === "--credential-stdin") {
|
|
@@ -2043,7 +2586,7 @@ function splitHeadlessArgv(argv) {
|
|
|
2043
2586
|
credentialStdin = true;
|
|
2044
2587
|
continue;
|
|
2045
2588
|
}
|
|
2046
|
-
if (token === "--env" || token === "--profile" || token === "--timeout" || token === "--attempt" || token === "--
|
|
2589
|
+
if (token === "--env" || token === "--profile" || token === "--timeout" || token === "--attempt" || token === "--date") {
|
|
2047
2590
|
const value = argv[index + 1];
|
|
2048
2591
|
if (value === void 0 || value.startsWith("--")) {
|
|
2049
2592
|
throw new InvocationError(`Invalid or incomplete command argument: ${token}`);
|
|
@@ -2058,10 +2601,48 @@ function splitHeadlessArgv(argv) {
|
|
|
2058
2601
|
}
|
|
2059
2602
|
positional.push(token);
|
|
2060
2603
|
}
|
|
2061
|
-
return { positional, options, credentialStdin };
|
|
2604
|
+
return { positional, options, credentialStdin, otpStdin };
|
|
2062
2605
|
}
|
|
2063
2606
|
function parseArguments(argv) {
|
|
2064
|
-
const
|
|
2607
|
+
const parsedInput = splitHeadlessArgv(argv.filter((value) => value !== "--stdin"));
|
|
2608
|
+
const commandVerb = parsedInput.positional[0];
|
|
2609
|
+
if (commandVerb === "settings" || commandVerb === "token") {
|
|
2610
|
+
const stdin = argv.includes("--stdin");
|
|
2611
|
+
if (argv.filter((value) => value === "--stdin").length > 1) throw new InvocationError("--stdin may be supplied only once.");
|
|
2612
|
+
const parsed = parsedInput;
|
|
2613
|
+
const [kind2, action] = parsed.positional;
|
|
2614
|
+
if (parsed.positional.length !== 2 || parsed.credentialStdin || parsed.otpStdin || [...parsed.options.keys()].some((key) => !["--env", "--profile", "--timeout"].includes(key)) || !(kind2 === "settings" && (action === "read" && !stdin || action === "save" && stdin) || kind2 === "token" && (action === "replace" && stdin || action === "read" && !stdin))) {
|
|
2615
|
+
throw new InvocationError("Use settings read, settings save --stdin, or token replace --stdin.");
|
|
2616
|
+
}
|
|
2617
|
+
const environment2 = parsed.options.get("--env"), profile2 = parsed.options.get("--profile");
|
|
2618
|
+
if (!environment2 || !profile2) throw new InvocationError("Headless commands require --env local|dev and --profile NAME.");
|
|
2619
|
+
validateCommandProfile(profile2);
|
|
2620
|
+
return { kind: kind2, action, environment: environment2, profile: profile2, ...parsed.options.has("--timeout") ? { timeoutMs: parseTimeout(parsed.options.get("--timeout")) } : {} };
|
|
2621
|
+
}
|
|
2622
|
+
if (argv.includes("--stdin")) throw new InvocationError("--stdin is only supported for Settings save and token replacement.");
|
|
2623
|
+
const { positional, options, credentialStdin, otpStdin } = parsedInput;
|
|
2624
|
+
if (positional[0] === "candle-export") {
|
|
2625
|
+
const instrument = positional[1]?.match(/^([A-Za-z][A-Za-z0-9]{0,31}):([^:\s]{1,100})$/);
|
|
2626
|
+
const date = options.get("--date");
|
|
2627
|
+
const parsedDate = date ? /* @__PURE__ */ new Date(`${date}T00:00:00Z`) : void 0;
|
|
2628
|
+
if (positional.length !== 2 || !instrument || credentialStdin || otpStdin || options.has("--attempt") || date !== void 0 && (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(parsedDate.getTime()) || parsedDate.toISOString().slice(0, 10) !== date || date < "2000-01-01")) {
|
|
2629
|
+
throw new InvocationError("Use candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD].");
|
|
2630
|
+
}
|
|
2631
|
+
const environment2 = options.get("--env");
|
|
2632
|
+
const profile2 = options.get("--profile");
|
|
2633
|
+
if (!environment2 || !profile2) throw new InvocationError("Headless commands require both --env local|dev and --profile NAME.");
|
|
2634
|
+
validateCommandProfile(profile2);
|
|
2635
|
+
return {
|
|
2636
|
+
kind: "candle-export",
|
|
2637
|
+
exchange: instrument[1].toUpperCase(),
|
|
2638
|
+
tradingSymbol: instrument[2].toUpperCase(),
|
|
2639
|
+
environment: environment2,
|
|
2640
|
+
profile: profile2,
|
|
2641
|
+
...date ? { date } : {},
|
|
2642
|
+
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
if (options.has("--date")) throw new InvocationError("--date is only supported for candle-export.");
|
|
2065
2646
|
if (positional[0] === "zerodha") {
|
|
2066
2647
|
const action = positional[1];
|
|
2067
2648
|
if (!["login", "observe", "otp", "status"].includes(action) || positional.length !== 2 || credentialStdin) {
|
|
@@ -2078,22 +2659,38 @@ function parseArguments(argv) {
|
|
|
2078
2659
|
if ((action === "observe" || action === "otp") && !attemptId) {
|
|
2079
2660
|
throw new InvocationError("--attempt ID is required for zerodha observe and otp. Use the attemptId returned by zerodha login.");
|
|
2080
2661
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
throw new InvocationError("--otp CODE is required for zerodha otp and is not supported for zerodha login. Start with zerodha login, then submit with zerodha otp --attempt ID --otp CODE.");
|
|
2662
|
+
if (action === "otp" && !otpStdin || otpStdin && action !== "otp") {
|
|
2663
|
+
throw new InvocationError("--otp-stdin is required for zerodha otp only. Start with zerodha login, then submit with zerodha otp --attempt ID --otp-stdin.");
|
|
2084
2664
|
}
|
|
2085
|
-
if (otp !== void 0 && !/^\d{6}$/.test(otp)) throw new InvocationError("OTP must be six digits.");
|
|
2086
2665
|
return {
|
|
2087
2666
|
kind: "zerodha",
|
|
2088
2667
|
action,
|
|
2089
2668
|
environment: environment2,
|
|
2090
2669
|
profile: profile2,
|
|
2091
2670
|
attemptId,
|
|
2092
|
-
|
|
2671
|
+
otpStdin,
|
|
2093
2672
|
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2094
2673
|
};
|
|
2095
2674
|
}
|
|
2096
|
-
if (
|
|
2675
|
+
if (positional[0] === "validate" && positional[1] === "zerodha-session") {
|
|
2676
|
+
if (positional.length !== 2 || credentialStdin) throw new InvocationError("Use validate zerodha-session with optional --attempt ID and --otp-stdin.");
|
|
2677
|
+
const environment2 = options.get("--env");
|
|
2678
|
+
const profile2 = options.get("--profile");
|
|
2679
|
+
if (!environment2 || !profile2) throw new InvocationError("Headless commands require both --env local|dev and --profile NAME.");
|
|
2680
|
+
validateCommandProfile(profile2);
|
|
2681
|
+
const attemptId = options.get("--attempt");
|
|
2682
|
+
if (attemptId !== void 0 && !/^[a-zA-Z0-9_-]{1,200}$/.test(attemptId)) throw new InvocationError("--attempt must be a valid Zerodha attempt ID.");
|
|
2683
|
+
if (otpStdin && !attemptId) throw new InvocationError("--otp-stdin requires an explicit --attempt ID from a prior login or validation.");
|
|
2684
|
+
return {
|
|
2685
|
+
kind: "validate-zerodha",
|
|
2686
|
+
environment: environment2,
|
|
2687
|
+
profile: profile2,
|
|
2688
|
+
attemptId,
|
|
2689
|
+
otpStdin,
|
|
2690
|
+
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
if (otpStdin || options.has("--attempt")) throw new InvocationError("--otp-stdin and --attempt are Zerodha options only.");
|
|
2097
2694
|
if (positional[0] === "credential") {
|
|
2098
2695
|
if (positional[1] !== "provision" || positional.length > 2) {
|
|
2099
2696
|
throw new InvocationError("Use `credential provision` to provision the protected SMS credential.");
|
|
@@ -2119,9 +2716,9 @@ function parseArguments(argv) {
|
|
|
2119
2716
|
throw new InvocationError("Unexpected positional argument; use --help for command syntax.");
|
|
2120
2717
|
}
|
|
2121
2718
|
} else if (positional.length === 0) {
|
|
2122
|
-
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session.");
|
|
2719
|
+
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session|zerodha-session.");
|
|
2123
2720
|
} else {
|
|
2124
|
-
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session.");
|
|
2721
|
+
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session|zerodha-session.");
|
|
2125
2722
|
}
|
|
2126
2723
|
const environment = options.get("--env");
|
|
2127
2724
|
const profile = options.get("--profile");
|
|
@@ -2138,11 +2735,11 @@ function validateCommandProfile(profile) {
|
|
|
2138
2735
|
}
|
|
2139
2736
|
}
|
|
2140
2737
|
function durationFor(command) {
|
|
2141
|
-
return command.kind === "validate" ? command.timeoutMs ?? 12e4 : command.kind === "credential-provision" ? 3e4 : command.timeoutMs ?? 3e4;
|
|
2738
|
+
return command.kind === "validate" || command.kind === "validate-zerodha" ? command.timeoutMs ?? 12e4 : command.kind === "credential-provision" ? 3e4 : command.timeoutMs ?? 3e4;
|
|
2142
2739
|
}
|
|
2143
2740
|
function errorDetails(error, signal) {
|
|
2144
2741
|
if (signal?.aborted) return { code: "deadline_exceeded", message: "The action exceeded its deadline.", exitCode: 1 };
|
|
2145
|
-
if (error instanceof InvocationError) {
|
|
2742
|
+
if (error instanceof InvocationError || error instanceof ProtectedInputError) {
|
|
2146
2743
|
return { code: error.code, message: error.message, exitCode: 2 };
|
|
2147
2744
|
}
|
|
2148
2745
|
if (error instanceof OperatorSessionRejectedError) {
|
|
@@ -2151,6 +2748,11 @@ function errorDetails(error, signal) {
|
|
|
2151
2748
|
if (error instanceof OperatorCredentialsRejectedError) {
|
|
2152
2749
|
return { code: "incorrect_credentials", message: "Gate rejected the Operator Login credentials.", exitCode: 1 };
|
|
2153
2750
|
}
|
|
2751
|
+
if (error instanceof ZerodhaLoginAttemptUnavailableError) return { code: "login_attempt_inactive", message: error.message, exitCode: 1 };
|
|
2752
|
+
if (error instanceof ZerodhaLoginFailedError) return { code: "zerodha_login_failed", message: error.message, exitCode: 1 };
|
|
2753
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string" && ["zerodha_session_absent", "zerodha_session_restoring", "zerodha_verification_unavailable", "zerodha_credentials_missing", "secret_store_unavailable", "operator_not_configured"].includes(error.code)) {
|
|
2754
|
+
return { code: error.code, message: error.message, exitCode: 1 };
|
|
2755
|
+
}
|
|
2154
2756
|
if (error instanceof SmsReadError) {
|
|
2155
2757
|
return { code: error.code, message: error.message, exitCode: 1 };
|
|
2156
2758
|
}
|
|
@@ -2196,18 +2798,28 @@ async function readProtectedInput(stdin, signal, label = "SMS credential") {
|
|
|
2196
2798
|
return credential;
|
|
2197
2799
|
}
|
|
2198
2800
|
async function runHeadlessCli(argv, dependencies = {}) {
|
|
2199
|
-
|
|
2801
|
+
let otp;
|
|
2802
|
+
let securePayload;
|
|
2803
|
+
const protect = (stream) => ({ write(chunk) {
|
|
2804
|
+
let output = otp ? chunk.replaceAll(otp, "[redacted]") : chunk;
|
|
2805
|
+
if (securePayload) output = output.replaceAll(securePayload, "[redacted]");
|
|
2806
|
+
return stream.write(output);
|
|
2807
|
+
} });
|
|
2808
|
+
const streams = { stdout: protect(dependencies.stdout ?? defaultOut), stderr: protect(dependencies.stderr ?? defaultErr) };
|
|
2200
2809
|
const started = performance.now();
|
|
2201
2810
|
let command;
|
|
2202
2811
|
let target;
|
|
2203
2812
|
let profile;
|
|
2204
2813
|
let actionApi;
|
|
2205
2814
|
let signal;
|
|
2815
|
+
let deadlineTimer;
|
|
2206
2816
|
let store;
|
|
2207
2817
|
try {
|
|
2208
2818
|
command = parseArguments(argv);
|
|
2209
2819
|
const timeoutMs = durationFor(command);
|
|
2210
|
-
|
|
2820
|
+
const deadline = new AbortController();
|
|
2821
|
+
deadlineTimer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
2822
|
+
signal = deadline.signal;
|
|
2211
2823
|
profile = command.profile;
|
|
2212
2824
|
try {
|
|
2213
2825
|
target = resolveCliTarget({ LMT_ENV: command.environment }, dependencies.configPath);
|
|
@@ -2230,6 +2842,33 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2230
2842
|
}, streams);
|
|
2231
2843
|
return 0;
|
|
2232
2844
|
}
|
|
2845
|
+
if (command.kind === "validate-zerodha") {
|
|
2846
|
+
const validation = await runZerodhaValidation({
|
|
2847
|
+
target,
|
|
2848
|
+
callerProfile: profile,
|
|
2849
|
+
store,
|
|
2850
|
+
timeoutMs,
|
|
2851
|
+
signal,
|
|
2852
|
+
attemptId: command.attemptId,
|
|
2853
|
+
...command.otpStdin ? { readOtp: (inputSignal) => readProtectedOtp(dependencies.stdin ?? process.stdin, inputSignal) } : {},
|
|
2854
|
+
...dependencies.fetchImpl ? { fetchImpl: dependencies.fetchImpl } : {}
|
|
2855
|
+
});
|
|
2856
|
+
emit({
|
|
2857
|
+
version: 1,
|
|
2858
|
+
command: "validate zerodha-session",
|
|
2859
|
+
outcome: validation.outcome,
|
|
2860
|
+
reason: validation.reason,
|
|
2861
|
+
target: validation.target,
|
|
2862
|
+
profile,
|
|
2863
|
+
observedOperator: validation.observedOperator,
|
|
2864
|
+
attemptId: validation.attemptId,
|
|
2865
|
+
observedState: validation.observedState,
|
|
2866
|
+
elapsedMs: validation.elapsedMs,
|
|
2867
|
+
metadata: { requestId: validation.requestId, buildId: validation.buildId },
|
|
2868
|
+
validation
|
|
2869
|
+
}, streams, validation.exitCode === 0 ? void 0 : `Zerodha Session validation ${validation.outcome}: ${validation.reason}. ${validation.nextStep ?? ""}`);
|
|
2870
|
+
return validation.exitCode;
|
|
2871
|
+
}
|
|
2233
2872
|
if (command.kind === "validate") {
|
|
2234
2873
|
const validation = await runOperatorSessionValidation({
|
|
2235
2874
|
target,
|
|
@@ -2267,15 +2906,66 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2267
2906
|
...dependencies.readLoginCredentials ? { readLoginCredentials: dependencies.readLoginCredentials } : {}
|
|
2268
2907
|
});
|
|
2269
2908
|
actionApi = await actions.getApi();
|
|
2909
|
+
if (command.kind === "settings" || command.kind === "token") {
|
|
2910
|
+
await actions.status();
|
|
2911
|
+
let payload;
|
|
2912
|
+
if (command.action !== "read") {
|
|
2913
|
+
securePayload = await readProtectedInput(dependencies.stdin ?? process.stdin, signal, "Settings JSON");
|
|
2914
|
+
try {
|
|
2915
|
+
payload = JSON.parse(securePayload);
|
|
2916
|
+
} catch {
|
|
2917
|
+
throw new InvocationError("Protected stdin must contain one JSON object.");
|
|
2918
|
+
}
|
|
2919
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new InvocationError("Protected stdin must contain one JSON object.");
|
|
2920
|
+
}
|
|
2921
|
+
const settings = command.kind === "settings" ? command.action === "read" ? { outcome: "read", settings: await actionApi.getSettings() } : await actionApi.saveSettings(payload) : void 0;
|
|
2922
|
+
const tokenMetadata = command.kind === "token" && command.action === "read" ? await actionApi.getTokenMetadata() : void 0;
|
|
2923
|
+
const token = command.kind === "token" && command.action === "replace" ? await actionApi.replaceToken(payload) : void 0;
|
|
2924
|
+
emit({
|
|
2925
|
+
version: 1,
|
|
2926
|
+
command: command.kind + " " + command.action,
|
|
2927
|
+
outcome: "success",
|
|
2928
|
+
reason: settings?.outcome ?? token?.outcome,
|
|
2929
|
+
...settings ? { settings: settings.settings } : {},
|
|
2930
|
+
...token ? { token } : {},
|
|
2931
|
+
...tokenMetadata ? { tokenMetadata } : {},
|
|
2932
|
+
target: { environment: target.environment, origin: target.origin },
|
|
2933
|
+
profile,
|
|
2934
|
+
elapsedMs: Math.round(performance.now() - started),
|
|
2935
|
+
metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
|
|
2936
|
+
}, streams);
|
|
2937
|
+
return 0;
|
|
2938
|
+
}
|
|
2939
|
+
if (command.kind === "candle-export") {
|
|
2940
|
+
await actions.status();
|
|
2941
|
+
const candleExport = await actionApi.exportCandles({
|
|
2942
|
+
exchange: command.exchange,
|
|
2943
|
+
tradingSymbol: command.tradingSymbol,
|
|
2944
|
+
...command.date ? { date: command.date } : {}
|
|
2945
|
+
});
|
|
2946
|
+
emit({
|
|
2947
|
+
version: 1,
|
|
2948
|
+
command: command.kind,
|
|
2949
|
+
outcome: "success",
|
|
2950
|
+
target: { environment: target.environment, origin: target.origin },
|
|
2951
|
+
profile,
|
|
2952
|
+
observedOperator: target.operatorId,
|
|
2953
|
+
candleExport,
|
|
2954
|
+
elapsedMs: Math.round(performance.now() - started),
|
|
2955
|
+
metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
|
|
2956
|
+
}, streams);
|
|
2957
|
+
return 0;
|
|
2958
|
+
}
|
|
2270
2959
|
if (command.kind === "zerodha") {
|
|
2271
2960
|
await actions.status();
|
|
2961
|
+
if (command.otpStdin) otp = await readProtectedOtp(dependencies.stdin ?? process.stdin, signal);
|
|
2272
2962
|
const zerodha = await runZerodhaAction(
|
|
2273
2963
|
actionApi,
|
|
2274
2964
|
command.action,
|
|
2275
2965
|
target.operatorId,
|
|
2276
2966
|
signal,
|
|
2277
2967
|
command.attemptId,
|
|
2278
|
-
|
|
2968
|
+
otp
|
|
2279
2969
|
);
|
|
2280
2970
|
const awaitingOtp = zerodha.status === "awaiting_otp";
|
|
2281
2971
|
emit({
|
|
@@ -2329,14 +3019,19 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2329
3019
|
if (error instanceof OperatorSessionRejectedError && store && actionApi && target && profile && !signal?.aborted) {
|
|
2330
3020
|
await store.saveSession(profile, target, actionApi.getCookies()).catch(() => void 0);
|
|
2331
3021
|
}
|
|
2332
|
-
const details = errorDetails(error, signal);
|
|
3022
|
+
const details = error instanceof SettingsActionError ? { code: error.code, message: error.message, exitCode: 1 } : errorDetails(error, signal);
|
|
3023
|
+
const recovery = error instanceof Error ? error : void 0;
|
|
3024
|
+
const attemptId = recovery?.attemptId ?? (command?.kind === "zerodha" ? command.attemptId : void 0);
|
|
2333
3025
|
emit({
|
|
2334
3026
|
version: 1,
|
|
2335
|
-
command: command?.kind === "zerodha" ?
|
|
2336
|
-
outcome: details.exitCode === 4 ? "blocked" : "fail",
|
|
3027
|
+
command: command?.kind === "zerodha" || command?.kind === "settings" || command?.kind === "token" ? `${command.kind} ${command.action}` : command?.kind === "credential-provision" ? "credential provision" : command?.kind === "validate-zerodha" ? "validate zerodha-session" : command?.kind === "validate" ? "validate operator-session" : command?.kind ?? "",
|
|
3028
|
+
outcome: error instanceof SettingsActionError && error.uncertain ? "uncertain" : details.exitCode === 4 ? "blocked" : "fail",
|
|
2337
3029
|
reason: details.code,
|
|
2338
3030
|
...target ? { target: { environment: target.environment, origin: target.origin } } : command ? { target: { environment: command.environment } } : {},
|
|
2339
3031
|
...profile ? { profile } : {},
|
|
3032
|
+
...attemptId ? { attemptId, recovery: recovery?.recovery ?? "Observe this exact attempt with the same target and profile; start explicitly only if Gate reports it inactive." } : {},
|
|
3033
|
+
...recovery?.observedState ? { observedState: recovery.observedState } : {},
|
|
3034
|
+
...recovery?.observedOperator ? { observedOperator: recovery.observedOperator } : {},
|
|
2340
3035
|
elapsedMs: Math.round(performance.now() - started),
|
|
2341
3036
|
metadata: {
|
|
2342
3037
|
requestId: actionApi?.responseMetadata().requestId ?? null,
|
|
@@ -2345,8 +3040,15 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2345
3040
|
error: { code: details.code, message: details.message }
|
|
2346
3041
|
}, streams, details.exitCode === 2 ? `Invalid CLI invocation: ${details.message}` : details.message);
|
|
2347
3042
|
return details.exitCode;
|
|
3043
|
+
} finally {
|
|
3044
|
+
otp = void 0;
|
|
3045
|
+
securePayload = void 0;
|
|
3046
|
+
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
2348
3047
|
}
|
|
2349
3048
|
}
|
|
3049
|
+
function hasArgumentOtp(argv) {
|
|
3050
|
+
return argv.some((token) => token === "--otp" || token.startsWith("--otp="));
|
|
3051
|
+
}
|
|
2350
3052
|
|
|
2351
3053
|
// src/index.ts
|
|
2352
3054
|
init_profile_name();
|
|
@@ -2368,26 +3070,45 @@ Usage (lmt is an alias of live-market-terminal):
|
|
|
2368
3070
|
live-market-terminal interactive credential provision [--env local|dev] [--profile NAME]
|
|
2369
3071
|
live-market-terminal login --env local|dev --profile NAME [--timeout DURATION]
|
|
2370
3072
|
live-market-terminal logout --env local|dev --profile NAME [--timeout DURATION]
|
|
3073
|
+
live-market-terminal candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD] --env local|dev --profile NAME [--timeout DURATION]
|
|
3074
|
+
live-market-terminal settings read --env local|dev --profile NAME
|
|
3075
|
+
live-market-terminal settings save --stdin --env local|dev --profile NAME
|
|
3076
|
+
live-market-terminal token read --env local|dev --profile NAME
|
|
3077
|
+
live-market-terminal token replace --stdin --env local|dev --profile NAME
|
|
2371
3078
|
live-market-terminal status --env local|dev --profile NAME [--timeout DURATION]
|
|
2372
3079
|
live-market-terminal zerodha login --env local|dev --profile NAME [--timeout DURATION]
|
|
2373
3080
|
live-market-terminal zerodha observe --attempt ID --env local|dev --profile NAME [--timeout DURATION]
|
|
2374
|
-
live-market-terminal zerodha otp --attempt ID --otp
|
|
3081
|
+
live-market-terminal zerodha otp --attempt ID --otp-stdin --env local|dev --profile NAME [--timeout DURATION]
|
|
2375
3082
|
live-market-terminal zerodha status --env local|dev --profile NAME [--timeout DURATION]
|
|
2376
3083
|
live-market-terminal validate operator-session --env local|dev --profile NAME [--timeout DURATION]
|
|
3084
|
+
live-market-terminal validate zerodha-session --env local|dev --profile NAME [--attempt ID [--otp-stdin]] [--timeout DURATION]
|
|
2377
3085
|
live-market-terminal credential provision --env local|dev --profile NAME --credential-stdin
|
|
2378
3086
|
|
|
2379
|
-
Headless options (--env, --profile, --timeout, --attempt, --otp, --credential-stdin) may appear before or after the command.
|
|
3087
|
+
Headless options (--env, --profile, --timeout, --attempt, --otp-stdin, --credential-stdin) may appear before or after the command.
|
|
2380
3088
|
|
|
2381
3089
|
From the monorepo root:
|
|
2382
3090
|
pnpm cli [--profile NAME] # interactive; --env defaults to local
|
|
2383
3091
|
pnpm cli --env dev --profile my-dev-session
|
|
2384
3092
|
pnpm -s cli:headless status --env local --profile default # build step runs quietly (-s)
|
|
2385
3093
|
|
|
3094
|
+
Candle Export publishes a new immutable workbook link and returns its context in candleExport JSON.
|
|
3095
|
+
Empty or unavailable sources are explained in the workbook. Export never starts Zerodha login or token recovery.
|
|
3096
|
+
|
|
3097
|
+
Settings save reads one JSON object from stdin: expectedSettingsVersion plus selected rowCount,
|
|
3098
|
+
marketProtection, entryCategories, or complete credentials. Omitted fields are preserved.
|
|
3099
|
+
Token replace reads {accessToken, expected, continueReplacement?} from protected stdin.
|
|
3100
|
+
Read token metadata through token read. A timeout after submission is uncertain; never retry blindly.
|
|
3101
|
+
|
|
2386
3102
|
Targets: local and dev only. Production is not permitted.
|
|
2387
3103
|
|
|
2388
|
-
Zerodha login is two steps: zerodha login, then zerodha otp --attempt ID --otp
|
|
3104
|
+
Zerodha login is two steps: zerodha login, then zerodha otp --attempt ID --otp-stdin.
|
|
2389
3105
|
Obtain the OTP from Kite after login returns awaiting_otp; expired attempts require a new login.
|
|
3106
|
+
OTP arguments (--otp CODE and --otp=CODE) are rejected. Pipe protected input; headless mode never prompts.
|
|
2390
3107
|
|
|
3108
|
+
Validate Zerodha Session starts a new attempt; resume the returned ID with --attempt ID.
|
|
3109
|
+
Supply --otp-stdin only when submitting a human OTP to that exact attempt.
|
|
3110
|
+
Validation retains the caller profile session and can replace Broker's active attempt or refresh its shared token.
|
|
3111
|
+
Interactive mode offers Validate Zerodha Session and explicit Resume Zerodha Validation.
|
|
2391
3112
|
Interactive mode restores a saved Operator Session when Gate still accepts it.
|
|
2392
3113
|
Headless commands write one JSON object per line to stdout; diagnostics go to stderr.
|
|
2393
3114
|
|
|
@@ -2402,8 +3123,9 @@ Options:
|
|
|
2402
3123
|
--env local|dev Target environment (interactive default: local; headless: required)
|
|
2403
3124
|
--profile NAME Client profile (default: default for interactive)
|
|
2404
3125
|
--timeout DURATION Action deadline, e.g. 45s, 2m, 30000ms (headless)
|
|
3126
|
+
--date YYYY-MM-DD Single IST date for candle-export; omitted: latest completed exchange session
|
|
2405
3127
|
--attempt ID Attempt ID returned by zerodha login (required for observe and otp)
|
|
2406
|
-
--otp
|
|
3128
|
+
--otp-stdin Read one six-digit OTP line from piped protected stdin (maximum 8 bytes)
|
|
2407
3129
|
--credential-stdin Read SMS credential from piped stdin (headless provision)
|
|
2408
3130
|
--help, -h Show this help
|
|
2409
3131
|
|
|
@@ -2423,6 +3145,10 @@ async function bootstrapCli(dependencies = {}) {
|
|
|
2423
3145
|
return;
|
|
2424
3146
|
}
|
|
2425
3147
|
const argv = normalizeArgv(process.argv.slice(2));
|
|
3148
|
+
if (hasArgumentOtp(argv)) {
|
|
3149
|
+
process.exitCode = await runHeadlessCli(argv);
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
2426
3152
|
if (wantsCliHelp(argv)) {
|
|
2427
3153
|
process.stdout.write(formatCliHelp());
|
|
2428
3154
|
return;
|
|
@@ -2477,6 +3203,7 @@ async function bootstrapCli(dependencies = {}) {
|
|
|
2477
3203
|
};
|
|
2478
3204
|
const app = render(createElement(LoginApp2, {
|
|
2479
3205
|
api,
|
|
3206
|
+
expectedOperatorId: target.operatorId,
|
|
2480
3207
|
actionTimeoutMs: 3e4,
|
|
2481
3208
|
async onResumeSession() {
|
|
2482
3209
|
return actions.resumeInteractiveSession();
|
|
@@ -2496,6 +3223,16 @@ async function bootstrapCli(dependencies = {}) {
|
|
|
2496
3223
|
});
|
|
2497
3224
|
return result;
|
|
2498
3225
|
},
|
|
3226
|
+
onValidateZerodha: (input) => runZerodhaValidation({
|
|
3227
|
+
target,
|
|
3228
|
+
callerProfile: profile,
|
|
3229
|
+
store,
|
|
3230
|
+
timeoutMs: 12e4,
|
|
3231
|
+
signal: input.signal,
|
|
3232
|
+
attemptId: input.attemptId,
|
|
3233
|
+
onObservation: input.onObservation,
|
|
3234
|
+
...input.otp ? { readOtp: async () => input.otp } : {}
|
|
3235
|
+
}),
|
|
2499
3236
|
onLogout: () => actions.logout()
|
|
2500
3237
|
}));
|
|
2501
3238
|
await app.waitUntilExit();
|