@ai-digital-factory/lmt-cli 0.1.2 → 0.1.3
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 +26 -5
- package/dist/index.js +695 -132
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,13 +15,15 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// ../../packages/contracts/src/index.ts
|
|
18
|
-
var OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS, OPERATOR_SESSION_EXPIRY_WARNING_MS, DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS, API_PATHS;
|
|
18
|
+
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, API_PATHS;
|
|
19
19
|
var init_src = __esm({
|
|
20
20
|
"../../packages/contracts/src/index.ts"() {
|
|
21
21
|
"use strict";
|
|
22
22
|
OPERATOR_SESSION_EXPIRY_TRACE_GRACE_SECONDS = 24 * 60 * 60;
|
|
23
23
|
OPERATOR_SESSION_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
|
|
24
24
|
DEFAULT_OPERATOR_SESSION_IDLE_HINT_SECONDS = 7 * 24 * 60 * 60;
|
|
25
|
+
ZERODHA_SESSION_ADMISSION_HEADER = "x-lmt-session-admission";
|
|
26
|
+
ZERODHA_SESSION_ADMISSION_VERIFY = "verify";
|
|
25
27
|
API_PATHS = {
|
|
26
28
|
health: "/api/health",
|
|
27
29
|
unlockOperatorSession: "/api/operator-session/unlock",
|
|
@@ -37,6 +39,7 @@ var init_src = __esm({
|
|
|
37
39
|
cancelOperatorLogin: "/api/operators/login/cancel",
|
|
38
40
|
zerodhaCredentials: "/api/zerodha/credentials",
|
|
39
41
|
entryCategorySettings: "/api/settings/entry-categories",
|
|
42
|
+
operatorSettings: "/api/settings",
|
|
40
43
|
marketProtectionSettings: "/api/settings/market-protection",
|
|
41
44
|
operatorEgress: "/api/operator-egress",
|
|
42
45
|
validateInstrument: "/api/instruments/validate",
|
|
@@ -48,6 +51,7 @@ var init_src = __esm({
|
|
|
48
51
|
previousInstruments: "/api/market-watch/previous-instruments",
|
|
49
52
|
candleHistoryLatest: "/api/candle-history/latest",
|
|
50
53
|
candleHistoryExport: "/api/candle-history/export",
|
|
54
|
+
candleExportOnDemand: "/api/candle-history/export/on-demand",
|
|
51
55
|
candleHistoryExportShare: "/api/candle-history/export/share",
|
|
52
56
|
candleHistory: "/api/candle-history",
|
|
53
57
|
zerodhaHistoricalCandles: "/api/zerodha/historical-candles",
|
|
@@ -111,7 +115,7 @@ function transportErrorDetail(error) {
|
|
|
111
115
|
}
|
|
112
116
|
return error.message;
|
|
113
117
|
}
|
|
114
|
-
async function requestOrThrow(request, unavailableMessage, signal, targetUrl) {
|
|
118
|
+
async function requestOrThrow(request, unavailableMessage, signal, targetUrl, secrets = []) {
|
|
115
119
|
try {
|
|
116
120
|
return await request();
|
|
117
121
|
} catch (error) {
|
|
@@ -119,7 +123,7 @@ async function requestOrThrow(request, unavailableMessage, signal, targetUrl) {
|
|
|
119
123
|
if (error instanceof OperatorSessionRejectedError || error instanceof ZerodhaLoginAttemptUnavailableError) throw error;
|
|
120
124
|
const detail = transportErrorDetail(error);
|
|
121
125
|
const at = targetUrl ? ` [${targetUrl}]` : "";
|
|
122
|
-
throw new Error(detail ? `${unavailableMessage} (${detail})${at}` : `${unavailableMessage}${at}
|
|
126
|
+
throw new Error(redactSecrets(detail ? `${unavailableMessage} (${detail})${at}` : `${unavailableMessage}${at}`, secrets));
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
function responseCookies(response) {
|
|
@@ -170,10 +174,18 @@ function parseEventFrame(frame) {
|
|
|
170
174
|
} catch {
|
|
171
175
|
throw new Error("Origin returned an invalid Zerodha login update.");
|
|
172
176
|
}
|
|
173
|
-
if (!event || typeof event !== "object" || !("type" in event)) {
|
|
177
|
+
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
178
|
throw new Error("Origin returned an invalid Zerodha login update.");
|
|
175
179
|
}
|
|
176
|
-
return event;
|
|
180
|
+
if (event.type === "attempt_error") return { type: event.type, attemptId: event.attemptId, error: event.error, message: event.message };
|
|
181
|
+
return {
|
|
182
|
+
type: event.type,
|
|
183
|
+
attemptId: event.attemptId,
|
|
184
|
+
operatorId: event.operatorId,
|
|
185
|
+
status: event.status,
|
|
186
|
+
...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts },
|
|
187
|
+
...event.message === void 0 ? {} : { message: event.message }
|
|
188
|
+
};
|
|
177
189
|
}
|
|
178
190
|
function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
179
191
|
let cookies = { ...options.cookies ?? {} };
|
|
@@ -189,8 +201,7 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
189
201
|
return AbortSignal.any(present);
|
|
190
202
|
}
|
|
191
203
|
function sessionSecrets() {
|
|
192
|
-
|
|
193
|
-
return [cookies.operatorSessionCookie, cookies.operatorSessionCookie.slice(cookies.operatorSessionCookie.indexOf("=") + 1)];
|
|
204
|
+
return [cookies.operatorSessionCookie, cookies.clientIdentityCookie].filter((cookie) => Boolean(cookie)).flatMap((cookie) => [cookie, cookie.slice(cookie.indexOf("=") + 1)]);
|
|
194
205
|
}
|
|
195
206
|
function headers(extra) {
|
|
196
207
|
const result = new Headers(extra);
|
|
@@ -223,7 +234,7 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
223
234
|
throw new OperatorSessionRejectedError(rejectionReason(body));
|
|
224
235
|
}
|
|
225
236
|
}
|
|
226
|
-
async function postJson(path4, body, explicitSignal) {
|
|
237
|
+
async function postJson(path4, body, explicitSignal, secrets = []) {
|
|
227
238
|
const signal = actionSignal(explicitSignal);
|
|
228
239
|
const url = `${origin}${path4}`;
|
|
229
240
|
const response = await requestOrThrow(
|
|
@@ -235,33 +246,44 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
235
246
|
}),
|
|
236
247
|
"Could not reach the selected Live Market Terminal Origin.",
|
|
237
248
|
signal,
|
|
238
|
-
url
|
|
249
|
+
url,
|
|
250
|
+
[...sessionSecrets(), ...otpValues, ...secrets]
|
|
239
251
|
);
|
|
240
252
|
rememberCookies(response);
|
|
241
253
|
await rejectAbsentSession(response, path4);
|
|
242
254
|
return response;
|
|
243
255
|
}
|
|
244
|
-
async function getJson(path4, explicitSignal) {
|
|
256
|
+
async function getJson(path4, explicitSignal, extraHeaders) {
|
|
245
257
|
const signal = actionSignal(explicitSignal);
|
|
246
258
|
const url = `${origin}${path4}`;
|
|
247
259
|
const response = await requestOrThrow(
|
|
248
260
|
() => fetchImpl(url, {
|
|
249
261
|
method: "GET",
|
|
250
|
-
headers: headers({ accept: "application/json" }),
|
|
262
|
+
headers: headers({ accept: "application/json", ...extraHeaders }),
|
|
251
263
|
cache: "no-store",
|
|
252
264
|
...signal ? { signal } : {}
|
|
253
265
|
}),
|
|
254
266
|
"Could not reach the selected Live Market Terminal Origin.",
|
|
255
267
|
signal,
|
|
256
|
-
url
|
|
268
|
+
url,
|
|
269
|
+
[...sessionSecrets(), ...otpValues]
|
|
257
270
|
);
|
|
258
271
|
rememberCookies(response);
|
|
259
272
|
await rejectAbsentSession(response, path4);
|
|
260
273
|
return response;
|
|
261
274
|
}
|
|
262
275
|
const api = {
|
|
276
|
+
async exportCandles(request) {
|
|
277
|
+
const response = await postJson(API_PATHS.candleExportOnDemand, request);
|
|
278
|
+
const body = await readJson(response);
|
|
279
|
+
if (!response.ok || !body || !("url" in body) || typeof body.url !== "string" || !("filename" in body)) {
|
|
280
|
+
throw new Error(messageFromBody(body, "Could not publish Candle Export.", sessionSecrets()));
|
|
281
|
+
}
|
|
282
|
+
return body;
|
|
283
|
+
},
|
|
263
284
|
setActionSignal(signal) {
|
|
264
285
|
actionDeadlineSignal = signal;
|
|
286
|
+
if (!signal) otpValues.clear();
|
|
265
287
|
},
|
|
266
288
|
setCookies(next) {
|
|
267
289
|
cookies = { ...next };
|
|
@@ -276,7 +298,7 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
276
298
|
return { ...metadata };
|
|
277
299
|
},
|
|
278
300
|
async unlock(username, password, signal) {
|
|
279
|
-
const response = await postJson(API_PATHS.unlockOperatorSession, { username, password }, signal);
|
|
301
|
+
const response = await postJson(API_PATHS.unlockOperatorSession, { username, password }, signal, [username, password]);
|
|
280
302
|
const body = await readJson(response);
|
|
281
303
|
if (!response.ok || !body || !("operatorSession" in body) || body.operatorSession !== "present") {
|
|
282
304
|
if (body && typeof body === "object" && "error" in body && body.error === "incorrect_credentials") {
|
|
@@ -366,8 +388,9 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
366
388
|
"Could not reach the selected Origin to start Login with Zerodha."
|
|
367
389
|
);
|
|
368
390
|
const body = await readJson(response);
|
|
369
|
-
if (!response.ok || !body || !("status" in body) || body.status !== "started" || typeof body.attemptId !== "string") {
|
|
370
|
-
|
|
391
|
+
if (!response.ok || !body || typeof body !== "object" || !("status" in body) || body.status !== "started" || typeof body.attemptId !== "string") {
|
|
392
|
+
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;
|
|
393
|
+
throw Object.assign(new Error(messageFromBody(body, "Could not start Login with Zerodha.", sessionSecrets())), code ? { code } : {});
|
|
371
394
|
}
|
|
372
395
|
otpValues.clear();
|
|
373
396
|
return { attemptId: body.attemptId, operatorId: body.operatorId };
|
|
@@ -379,9 +402,9 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
379
402
|
"Could not reach the selected Origin to submit the OTP."
|
|
380
403
|
);
|
|
381
404
|
const body = await readJson(response);
|
|
382
|
-
if (!response.ok || !body || !("status" in body) || body.status !== "loading") {
|
|
405
|
+
if (!response.ok || !body || typeof body !== "object" || !("status" in body) || body.status !== "loading") {
|
|
383
406
|
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")) {
|
|
407
|
+
if (body && typeof body === "object" && "error" in body && (body.error === "login_attempt_not_found" || body.error === "login_attempt_expired")) {
|
|
385
408
|
throw new ZerodhaLoginAttemptUnavailableError(message);
|
|
386
409
|
}
|
|
387
410
|
throw new Error(message);
|
|
@@ -415,12 +438,22 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
415
438
|
throw new Error(message);
|
|
416
439
|
}
|
|
417
440
|
const reader = response.body.getReader();
|
|
441
|
+
const cancelReader = () => {
|
|
442
|
+
void reader.cancel().catch(() => void 0);
|
|
443
|
+
};
|
|
444
|
+
requestSignal.addEventListener("abort", cancelReader, { once: true });
|
|
418
445
|
const decoder = new TextDecoder();
|
|
419
446
|
let buffer = "";
|
|
420
447
|
let completed = false;
|
|
421
448
|
try {
|
|
422
449
|
while (!requestSignal.aborted) {
|
|
423
|
-
|
|
450
|
+
let chunk;
|
|
451
|
+
try {
|
|
452
|
+
chunk = await reader.read();
|
|
453
|
+
} catch {
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
const { done, value } = chunk;
|
|
424
457
|
buffer += decoder.decode(value, { stream: !done });
|
|
425
458
|
let boundary = buffer.search(/\r?\n\r?\n/);
|
|
426
459
|
while (boundary >= 0) {
|
|
@@ -450,6 +483,7 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
450
483
|
if (done) break;
|
|
451
484
|
}
|
|
452
485
|
} finally {
|
|
486
|
+
requestSignal.removeEventListener("abort", cancelReader);
|
|
453
487
|
await reader.cancel().catch(() => void 0);
|
|
454
488
|
}
|
|
455
489
|
if (completed || requestSignal.aborted) return;
|
|
@@ -458,12 +492,13 @@ function createGateApi(origin, fetchImpl = fetch, options = {}) {
|
|
|
458
492
|
},
|
|
459
493
|
async confirmZerodhaSession() {
|
|
460
494
|
const response = await requestOrThrow(
|
|
461
|
-
() => getJson(API_PATHS.zerodhaSession),
|
|
495
|
+
() => getJson(API_PATHS.zerodhaSession, void 0, { [ZERODHA_SESSION_ADMISSION_HEADER]: ZERODHA_SESSION_ADMISSION_VERIFY }),
|
|
462
496
|
"Could not verify the Zerodha Session with the selected Origin."
|
|
463
497
|
);
|
|
464
498
|
const body = await readJson(response);
|
|
465
|
-
if (!response.ok || !body || !("zerodhaSession" in body) || body.zerodhaSession !== "present") {
|
|
466
|
-
|
|
499
|
+
if (!response.ok || !body || typeof body !== "object" || !("zerodhaSession" in body) || body.zerodhaSession !== "present") {
|
|
500
|
+
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";
|
|
501
|
+
throw Object.assign(new Error(messageFromBody(body, "Zerodha Session is not present on the Broker.", sessionSecrets())), { code });
|
|
467
502
|
}
|
|
468
503
|
return body;
|
|
469
504
|
}
|
|
@@ -515,6 +550,112 @@ var init_gate_api = __esm({
|
|
|
515
550
|
}
|
|
516
551
|
});
|
|
517
552
|
|
|
553
|
+
// src/action-deadline.ts
|
|
554
|
+
function withinActionDeadline(operation, signal, deadlineMessage) {
|
|
555
|
+
return new Promise((resolve, reject) => {
|
|
556
|
+
const abort = () => reject(new Error(deadlineMessage));
|
|
557
|
+
if (signal.aborted) {
|
|
558
|
+
void operation.catch(() => void 0);
|
|
559
|
+
abort();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
563
|
+
operation.then(
|
|
564
|
+
(value) => {
|
|
565
|
+
signal.removeEventListener("abort", abort);
|
|
566
|
+
resolve(value);
|
|
567
|
+
},
|
|
568
|
+
(error) => {
|
|
569
|
+
signal.removeEventListener("abort", abort);
|
|
570
|
+
reject(error);
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
var init_action_deadline = __esm({
|
|
576
|
+
"src/action-deadline.ts"() {
|
|
577
|
+
"use strict";
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// src/zerodha-actions.ts
|
|
582
|
+
async function runZerodhaAction(api, action, operatorId, signal, attemptId, otp, options = {}) {
|
|
583
|
+
const started = Date.now();
|
|
584
|
+
let observedState;
|
|
585
|
+
let observedOperator2;
|
|
586
|
+
api.setActionSignal?.(signal);
|
|
587
|
+
try {
|
|
588
|
+
if (signal.aborted) throw new Error("Zerodha login observation exceeded its deadline.");
|
|
589
|
+
if (action === "status") {
|
|
590
|
+
const session = await withinActionDeadline(api.confirmZerodhaSession(), signal, "Zerodha login observation exceeded its deadline.");
|
|
591
|
+
if (session.operatorId && session.operatorId !== operatorId) throw new Error("Origin returned an unexpected Zerodha Session Operator.");
|
|
592
|
+
return { status: "succeeded", observedState: "present", ...session.operatorId ? { observedOperator: session.operatorId } : {}, elapsedMs: Date.now() - started };
|
|
593
|
+
}
|
|
594
|
+
if (action === "login") {
|
|
595
|
+
const attempt = await withinActionDeadline(api.startLogin(), signal, "Zerodha login observation exceeded its deadline.");
|
|
596
|
+
attemptId = attempt.attemptId;
|
|
597
|
+
if (attempt.operatorId !== operatorId) throw new Error("Zerodha login reached an unexpected Operator.");
|
|
598
|
+
}
|
|
599
|
+
if (!attemptId) throw new Error("Zerodha login requires an attempt identifier.");
|
|
600
|
+
if (action === "otp") {
|
|
601
|
+
if (!otp) throw new Error("Zerodha OTP input is required.");
|
|
602
|
+
await withinActionDeadline(api.submitOtp(attemptId, otp), signal, "Zerodha login observation exceeded its deadline.");
|
|
603
|
+
}
|
|
604
|
+
const observer = new AbortController();
|
|
605
|
+
const observationSignal = AbortSignal.any([signal, observer.signal]);
|
|
606
|
+
let result;
|
|
607
|
+
try {
|
|
608
|
+
await withinActionDeadline(api.watchLoginAttempt(attemptId, (event) => {
|
|
609
|
+
if (observer.signal.aborted || signal.aborted) return;
|
|
610
|
+
if (event.attemptId !== attemptId || event.type === "status" && event.operatorId !== operatorId) {
|
|
611
|
+
throw new Error("Origin returned an unexpected Zerodha login attempt.");
|
|
612
|
+
}
|
|
613
|
+
if (event.type === "status") {
|
|
614
|
+
observedState = event.status;
|
|
615
|
+
observedOperator2 = event.operatorId;
|
|
616
|
+
}
|
|
617
|
+
options.onObservation?.(event);
|
|
618
|
+
if (event.type === "status" && (event.status === "awaiting_otp" || event.status === "succeeded")) {
|
|
619
|
+
result = {
|
|
620
|
+
status: event.status,
|
|
621
|
+
attemptId,
|
|
622
|
+
observedOperator: observedOperator2,
|
|
623
|
+
observedState,
|
|
624
|
+
...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts },
|
|
625
|
+
...event.message ? { message: event.message } : {}
|
|
626
|
+
};
|
|
627
|
+
observer.abort();
|
|
628
|
+
}
|
|
629
|
+
}, observationSignal), observationSignal, "Zerodha login observation exceeded its deadline.");
|
|
630
|
+
} catch (error) {
|
|
631
|
+
if (!observer.signal.aborted || !result) throw error;
|
|
632
|
+
}
|
|
633
|
+
if (signal.aborted || !result) throw new Error("Zerodha login observation exceeded its deadline.");
|
|
634
|
+
if (result.status === "succeeded") {
|
|
635
|
+
const session = await withinActionDeadline(api.confirmZerodhaSession(), signal, "Zerodha login observation exceeded its deadline.");
|
|
636
|
+
if (session.operatorId && session.operatorId !== operatorId) throw new Error("Origin returned an unexpected Zerodha Session Operator.");
|
|
637
|
+
}
|
|
638
|
+
return { ...result, elapsedMs: Date.now() - started };
|
|
639
|
+
} catch (cause) {
|
|
640
|
+
const error = cause instanceof Error ? cause : new Error("Zerodha action failed.");
|
|
641
|
+
Object.assign(error, {
|
|
642
|
+
...attemptId ? { attemptId, recovery: "Observe this exact attempt with the same target and profile; start explicitly only if Gate reports it inactive." } : {},
|
|
643
|
+
...observedState ? { observedState } : {},
|
|
644
|
+
...observedOperator2 ? { observedOperator: observedOperator2 } : {},
|
|
645
|
+
elapsedMs: Date.now() - started
|
|
646
|
+
});
|
|
647
|
+
throw error;
|
|
648
|
+
} finally {
|
|
649
|
+
api.setActionSignal?.();
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
var init_zerodha_actions = __esm({
|
|
653
|
+
"src/zerodha-actions.ts"() {
|
|
654
|
+
"use strict";
|
|
655
|
+
init_action_deadline();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
518
659
|
// src/credential-app.tsx
|
|
519
660
|
var credential_app_exports = {};
|
|
520
661
|
__export(credential_app_exports, {
|
|
@@ -581,6 +722,24 @@ __export(app_exports, {
|
|
|
581
722
|
import { useEffect, useRef, useState as useState2 } from "react";
|
|
582
723
|
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
583
724
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
725
|
+
function formatZerodhaValidationReport(report) {
|
|
726
|
+
return [
|
|
727
|
+
"Scenario: zerodha-session",
|
|
728
|
+
`Outcome: ${report.outcome}`,
|
|
729
|
+
`Reason: ${report.reason}`,
|
|
730
|
+
`Target: ${report.target.environment} ${report.target.origin}`,
|
|
731
|
+
`Profile: ${report.profile}`,
|
|
732
|
+
`Observed Operator: ${report.observedOperator ?? "not observed"}`,
|
|
733
|
+
`Observed state: ${report.observedState ?? "not observed"}`,
|
|
734
|
+
`Attempt: ${report.attemptId ?? "not observed"}`,
|
|
735
|
+
`Elapsed: ${report.elapsedMs} ms`,
|
|
736
|
+
`Request ID: ${report.requestId ?? "unavailable"}`,
|
|
737
|
+
`Build ID: ${report.buildId ?? "unavailable"}`,
|
|
738
|
+
...report.failedOtpAttempts === void 0 ? [] : [`Failed OTP attempts: ${report.failedOtpAttempts}`],
|
|
739
|
+
...report.message ? [report.message] : [],
|
|
740
|
+
...report.nextStep ? [`Next step: ${report.nextStep}`] : []
|
|
741
|
+
].join("\n");
|
|
742
|
+
}
|
|
584
743
|
function formatValidationReport(report) {
|
|
585
744
|
const lines = [
|
|
586
745
|
`Outcome: ${report.outcome}`,
|
|
@@ -603,14 +762,18 @@ function formatValidationReport(report) {
|
|
|
603
762
|
}
|
|
604
763
|
return lines.join("\n");
|
|
605
764
|
}
|
|
606
|
-
function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperatorActivity, onStatus, onValidate, onLogout }) {
|
|
765
|
+
function LoginApp({ api, expectedOperatorId = "OPERATOR_1", actionTimeoutMs = 3e4, onResumeSession, onReportOperatorActivity, onStatus, onValidate, onValidateZerodha, onLogout }) {
|
|
607
766
|
const [step, setStep] = useState2(onResumeSession ? "checking" : "username");
|
|
608
767
|
const [username, setUsername] = useState2("");
|
|
609
768
|
const [password, setPassword] = useState2("");
|
|
610
769
|
const [otp, setOtp] = useState2("");
|
|
770
|
+
const [progress, setProgress] = useState2(null);
|
|
611
771
|
const [error, setError] = useState2("");
|
|
612
772
|
const [menuMessage, setMenuMessage] = useState2("");
|
|
613
773
|
const [validationReport, setValidationReport] = useState2(null);
|
|
774
|
+
const [zerodhaValidationReport, setZerodhaValidationReport] = useState2(null);
|
|
775
|
+
const [resumeAttemptInput, setResumeAttemptInput] = useState2("");
|
|
776
|
+
const validatingZerodha = useRef(false);
|
|
614
777
|
const [selectedMenuIndex, setSelectedMenuIndex] = useState2(0);
|
|
615
778
|
const attemptId = useRef(null);
|
|
616
779
|
const watcher = useRef(null);
|
|
@@ -695,80 +858,105 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
695
858
|
setStep("username");
|
|
696
859
|
}
|
|
697
860
|
}
|
|
698
|
-
function applyLoginEvent(event, signal) {
|
|
699
|
-
if (signal.aborted) return;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
861
|
+
function applyLoginEvent(event, signal, action) {
|
|
862
|
+
if (signal.aborted || event.type !== "status") return;
|
|
863
|
+
attemptId.current = event.attemptId;
|
|
864
|
+
setProgress({
|
|
865
|
+
status: event.status === "succeeded" ? "succeeded" : "awaiting_otp",
|
|
866
|
+
attemptId: event.attemptId,
|
|
867
|
+
observedState: event.status,
|
|
868
|
+
observedOperator: event.operatorId,
|
|
869
|
+
...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts }
|
|
870
|
+
});
|
|
871
|
+
if (event.status === "loading") setStep(action === "login" ? "waiting" : "verifying");
|
|
872
|
+
}
|
|
873
|
+
async function runLoginAction(action, controller, code) {
|
|
874
|
+
try {
|
|
875
|
+
const result = await runZerodhaAction(
|
|
876
|
+
api,
|
|
877
|
+
action,
|
|
878
|
+
expectedOperatorId,
|
|
879
|
+
controller.signal,
|
|
880
|
+
action === "otp" ? attemptId.current ?? void 0 : void 0,
|
|
881
|
+
code,
|
|
882
|
+
{ onObservation: (event) => applyLoginEvent(event, controller.signal, action) }
|
|
883
|
+
);
|
|
884
|
+
if (controller.signal.aborted) return;
|
|
885
|
+
attemptId.current = result.attemptId ?? null;
|
|
886
|
+
setProgress(result);
|
|
703
887
|
setOtp("");
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
888
|
+
if (result.status === "awaiting_otp") {
|
|
889
|
+
setError(result.message ?? "");
|
|
890
|
+
setStep("otp");
|
|
891
|
+
} else {
|
|
892
|
+
clearZerodhaLoginDeadline(controller);
|
|
893
|
+
setStep("success");
|
|
894
|
+
}
|
|
895
|
+
} catch (cause) {
|
|
896
|
+
if (controller.signal.aborted) return;
|
|
897
|
+
clearZerodhaLoginDeadline(controller);
|
|
898
|
+
setOtp("");
|
|
899
|
+
const recovery = cause;
|
|
900
|
+
if (recovery?.attemptId) attemptId.current = recovery.attemptId;
|
|
901
|
+
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."}` : ""}`);
|
|
902
|
+
setStep(cause instanceof OperatorSessionRejectedError ? "username" : "menu");
|
|
709
903
|
}
|
|
710
904
|
}
|
|
711
905
|
async function beginLogin() {
|
|
906
|
+
validatingZerodha.current = false;
|
|
907
|
+
setZerodhaValidationReport(null);
|
|
712
908
|
setError("");
|
|
909
|
+
setProgress(null);
|
|
910
|
+
attemptId.current = null;
|
|
713
911
|
setStep("starting");
|
|
714
912
|
const controller = new AbortController();
|
|
715
913
|
watcher.current?.abort();
|
|
716
914
|
watcher.current = controller;
|
|
717
|
-
api.setActionSignal?.(controller.signal);
|
|
718
915
|
const timer = setTimeout(() => {
|
|
719
916
|
if (zerodhaLoginDeadline.current?.controller !== controller) return;
|
|
720
917
|
controller.abort();
|
|
721
918
|
clearZerodhaLoginDeadline(controller);
|
|
722
|
-
|
|
919
|
+
setOtp("");
|
|
920
|
+
setError(`Login with Zerodha exceeded its deadline.${attemptId.current ? ` Observe attempt ${attemptId.current} with the same target and profile.` : ""}`);
|
|
723
921
|
setStep("menu");
|
|
724
922
|
}, actionTimeoutMs);
|
|
725
923
|
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
|
-
}
|
|
924
|
+
await runLoginAction("login", controller);
|
|
750
925
|
}
|
|
751
926
|
async function submitOtp() {
|
|
752
927
|
if (!/^\d{6}$/.test(otp)) {
|
|
753
928
|
setError("Enter the 6-digit Zerodha OTP.");
|
|
754
929
|
return;
|
|
755
930
|
}
|
|
756
|
-
if (!attemptId.current) {
|
|
931
|
+
if (!attemptId.current || !watcher.current || watcher.current.signal.aborted) {
|
|
932
|
+
setOtp("");
|
|
757
933
|
setError("The Zerodha login attempt is no longer active.");
|
|
758
934
|
setStep("menu");
|
|
759
935
|
return;
|
|
760
936
|
}
|
|
937
|
+
const code = otp;
|
|
938
|
+
setOtp("");
|
|
761
939
|
setError("");
|
|
762
940
|
setStep("verifying");
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
941
|
+
if (validatingZerodha.current) await runValidation(watcher.current, attemptId.current, code);
|
|
942
|
+
else await runLoginAction("otp", watcher.current, code);
|
|
943
|
+
}
|
|
944
|
+
function checkZerodhaSession() {
|
|
945
|
+
setError("");
|
|
946
|
+
setMenuMessage("");
|
|
947
|
+
setStep("checking");
|
|
948
|
+
const controller = new AbortController();
|
|
949
|
+
watcher.current = controller;
|
|
950
|
+
const timer = setTimeout(() => controller.abort(), actionTimeoutMs);
|
|
951
|
+
void runZerodhaAction(api, "status", expectedOperatorId, controller.signal).then(() => {
|
|
952
|
+
setMenuMessage("Zerodha Session confirmed by Gate.");
|
|
953
|
+
setStep("menu");
|
|
954
|
+
}).catch((cause) => {
|
|
955
|
+
setError(cause instanceof Error ? cause.message : "Could not check the Zerodha Session.");
|
|
956
|
+
setStep(cause instanceof OperatorSessionRejectedError ? "username" : "menu");
|
|
957
|
+
}).finally(() => {
|
|
958
|
+
clearTimeout(timer);
|
|
959
|
+
});
|
|
772
960
|
}
|
|
773
961
|
function checkOperatorSession() {
|
|
774
962
|
if (!onStatus) return;
|
|
@@ -805,6 +993,81 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
805
993
|
setStep("menu");
|
|
806
994
|
});
|
|
807
995
|
}
|
|
996
|
+
async function runValidation(controller, selectedAttempt, code) {
|
|
997
|
+
if (!onValidateZerodha) return;
|
|
998
|
+
try {
|
|
999
|
+
const result = await onValidateZerodha({
|
|
1000
|
+
attemptId: selectedAttempt,
|
|
1001
|
+
otp: code,
|
|
1002
|
+
signal: controller.signal,
|
|
1003
|
+
onObservation: (event) => applyLoginEvent(event, controller.signal, code ? "otp" : "login")
|
|
1004
|
+
});
|
|
1005
|
+
if (watcher.current !== controller) return;
|
|
1006
|
+
setOtp("");
|
|
1007
|
+
setZerodhaValidationReport(result);
|
|
1008
|
+
attemptId.current = result.attemptId ?? null;
|
|
1009
|
+
if (!controller.signal.aborted && result.attemptId && ["awaiting_otp", "otp_rejected"].includes(result.reason)) {
|
|
1010
|
+
setStep("otp");
|
|
1011
|
+
} else {
|
|
1012
|
+
clearZerodhaLoginDeadline(controller);
|
|
1013
|
+
validatingZerodha.current = false;
|
|
1014
|
+
if (result.reason === "operator_login_required") {
|
|
1015
|
+
setUsername("");
|
|
1016
|
+
setPassword("");
|
|
1017
|
+
}
|
|
1018
|
+
setStep(result.reason === "operator_login_required" ? "username" : "menu");
|
|
1019
|
+
}
|
|
1020
|
+
} catch (cause) {
|
|
1021
|
+
if (watcher.current !== controller) return;
|
|
1022
|
+
clearZerodhaLoginDeadline(controller);
|
|
1023
|
+
validatingZerodha.current = false;
|
|
1024
|
+
setOtp("");
|
|
1025
|
+
const message = cause instanceof Error ? cause.message : "Zerodha Session validation failed.";
|
|
1026
|
+
setError(code ? message.replaceAll(code, "[REDACTED]") : message);
|
|
1027
|
+
setStep("menu");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async function beginValidation(selectedAttempt) {
|
|
1031
|
+
watcher.current?.abort();
|
|
1032
|
+
clearZerodhaLoginDeadline();
|
|
1033
|
+
const controller = new AbortController();
|
|
1034
|
+
const started = Date.now();
|
|
1035
|
+
watcher.current = controller;
|
|
1036
|
+
validatingZerodha.current = true;
|
|
1037
|
+
attemptId.current = selectedAttempt ?? null;
|
|
1038
|
+
setOtp("");
|
|
1039
|
+
setProgress(null);
|
|
1040
|
+
setError("");
|
|
1041
|
+
setMenuMessage("");
|
|
1042
|
+
setValidationReport(null);
|
|
1043
|
+
setZerodhaValidationReport(null);
|
|
1044
|
+
setStep("validatingZerodha");
|
|
1045
|
+
const timer = setTimeout(() => {
|
|
1046
|
+
if (zerodhaLoginDeadline.current?.controller !== controller) return;
|
|
1047
|
+
controller.abort();
|
|
1048
|
+
clearZerodhaLoginDeadline(controller);
|
|
1049
|
+
validatingZerodha.current = false;
|
|
1050
|
+
setOtp("");
|
|
1051
|
+
setZerodhaValidationReport((previous) => previous ? {
|
|
1052
|
+
...previous,
|
|
1053
|
+
outcome: "fail",
|
|
1054
|
+
reason: "deadline_exceeded",
|
|
1055
|
+
exitCode: 1,
|
|
1056
|
+
message: "The Zerodha Session validation exceeded its deadline.",
|
|
1057
|
+
elapsedMs: Date.now() - started,
|
|
1058
|
+
nextStep: `Observe attempt ${previous.attemptId} with the same target and profile; explicitly start again only if Gate reports it inactive.`
|
|
1059
|
+
} : previous);
|
|
1060
|
+
setError(`Zerodha Session validation exceeded its deadline.${attemptId.current ? ` Observe attempt ${attemptId.current} with the same target and profile.` : ""}`);
|
|
1061
|
+
setStep("menu");
|
|
1062
|
+
}, actionTimeoutMs);
|
|
1063
|
+
zerodhaLoginDeadline.current = { controller, timer };
|
|
1064
|
+
await runValidation(controller, selectedAttempt);
|
|
1065
|
+
}
|
|
1066
|
+
function resumeValidation() {
|
|
1067
|
+
setError("");
|
|
1068
|
+
setResumeAttemptInput("");
|
|
1069
|
+
setStep("validationAttempt");
|
|
1070
|
+
}
|
|
808
1071
|
function logoutOperatorSession() {
|
|
809
1072
|
if (!onLogout) return;
|
|
810
1073
|
setError("");
|
|
@@ -814,6 +1077,7 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
814
1077
|
setPassword("");
|
|
815
1078
|
setMenuMessage("");
|
|
816
1079
|
setValidationReport(null);
|
|
1080
|
+
setZerodhaValidationReport(null);
|
|
817
1081
|
setStep("username");
|
|
818
1082
|
}).catch((cause) => {
|
|
819
1083
|
setError(cause instanceof Error ? cause.message : "Could not log out the Operator Session.");
|
|
@@ -826,7 +1090,14 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
826
1090
|
} },
|
|
827
1091
|
...onStatus ? [{ shortcut: "2", label: "Check Operator Session", run: checkOperatorSession }] : [],
|
|
828
1092
|
...onValidate ? [{ shortcut: "3", label: "Validate Operator Session", run: validateOperatorSession }] : [],
|
|
829
|
-
...onLogout ? [{ shortcut: "4", label: "Logout Operator Session", run: logoutOperatorSession }] : []
|
|
1093
|
+
...onLogout ? [{ shortcut: "4", label: "Logout Operator Session", run: logoutOperatorSession }] : [],
|
|
1094
|
+
{ shortcut: "5", label: "Check Zerodha Session", run: checkZerodhaSession },
|
|
1095
|
+
...onValidateZerodha ? [
|
|
1096
|
+
{ shortcut: "6", label: "Validate Zerodha Session", run: () => {
|
|
1097
|
+
void beginValidation();
|
|
1098
|
+
} },
|
|
1099
|
+
{ shortcut: "7", label: "Resume Zerodha Validation", run: resumeValidation }
|
|
1100
|
+
] : []
|
|
830
1101
|
];
|
|
831
1102
|
const activeMenuIndex = Math.min(selectedMenuIndex, menuActions.length - 1);
|
|
832
1103
|
useInput2((input, key) => {
|
|
@@ -864,6 +1135,15 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
864
1135
|
}
|
|
865
1136
|
return;
|
|
866
1137
|
}
|
|
1138
|
+
if (step === "validationAttempt") {
|
|
1139
|
+
if (key.escape) setStep("menu");
|
|
1140
|
+
else if (key.return) {
|
|
1141
|
+
if (!/^[a-zA-Z0-9_-]{1,200}$/.test(resumeAttemptInput)) setError("Enter an explicit valid Zerodha attempt ID.");
|
|
1142
|
+
else void beginValidation(resumeAttemptInput);
|
|
1143
|
+
} else if (key.backspace || key.delete) setResumeAttemptInput((value) => value.slice(0, -1));
|
|
1144
|
+
else if (input && !key.ctrl && !key.meta) setResumeAttemptInput((value) => (value + input).slice(0, 200));
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
867
1147
|
if (step === "menu") {
|
|
868
1148
|
if (key.upArrow) {
|
|
869
1149
|
setSelectedMenuIndex((index) => (index - 1 + menuActions.length) % menuActions.length);
|
|
@@ -878,7 +1158,13 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
878
1158
|
return;
|
|
879
1159
|
}
|
|
880
1160
|
if (step === "otp") {
|
|
881
|
-
if (key.
|
|
1161
|
+
if (key.escape && validatingZerodha.current) {
|
|
1162
|
+
watcher.current?.abort();
|
|
1163
|
+
clearZerodhaLoginDeadline();
|
|
1164
|
+
validatingZerodha.current = false;
|
|
1165
|
+
setOtp("");
|
|
1166
|
+
setStep("menu");
|
|
1167
|
+
} else if (key.return) void submitOtp();
|
|
882
1168
|
else if (key.backspace || key.delete) setOtp((value) => value.slice(0, -1));
|
|
883
1169
|
else if (input) setOtp((value) => (value + input.replace(/\D/g, "")).slice(0, 6));
|
|
884
1170
|
}
|
|
@@ -923,6 +1209,16 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
923
1209
|
] }),
|
|
924
1210
|
step === "checking" && /* @__PURE__ */ jsx2(Text2, { children: "Checking Operator Session\u2026" }),
|
|
925
1211
|
step === "validating" && /* @__PURE__ */ jsx2(Text2, { children: "Validating Operator Session\u2026" }),
|
|
1212
|
+
step === "validatingZerodha" && /* @__PURE__ */ jsx2(Text2, { children: "Validating Zerodha Session\u2026" }),
|
|
1213
|
+
step === "validationAttempt" && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1214
|
+
/* @__PURE__ */ jsx2(Text2, { children: "Zerodha attempt ID" }),
|
|
1215
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1216
|
+
resumeAttemptInput,
|
|
1217
|
+
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " })
|
|
1218
|
+
] }),
|
|
1219
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Enter to observe this attempt \xB7 Esc to return" })
|
|
1220
|
+
] }),
|
|
1221
|
+
zerodhaValidationReport ? /* @__PURE__ */ jsx2(Text2, { children: formatZerodhaValidationReport(zerodhaValidationReport) }) : null,
|
|
926
1222
|
step === "loggingOut" && /* @__PURE__ */ jsx2(Text2, { children: "Logging out of the Operator Session\u2026" }),
|
|
927
1223
|
step === "starting" && /* @__PURE__ */ jsx2(Text2, { children: "Starting Login with Zerodha\u2026" }),
|
|
928
1224
|
step === "waiting" && /* @__PURE__ */ jsx2(Text2, { children: "Connecting to Zerodha\u2026" }),
|
|
@@ -931,9 +1227,22 @@ function LoginApp({ api, actionTimeoutMs = 3e4, onResumeSession, onReportOperato
|
|
|
931
1227
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
932
1228
|
"\u2022".repeat(otp.length),
|
|
933
1229
|
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " })
|
|
934
|
-
] })
|
|
1230
|
+
] }),
|
|
1231
|
+
validatingZerodha.current ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Enter to submit \xB7 Esc to return and resume later" }) : null
|
|
935
1232
|
] }),
|
|
936
1233
|
step === "verifying" && /* @__PURE__ */ jsx2(Text2, { children: "Verifying Zerodha login\u2026" }),
|
|
1234
|
+
progress?.attemptId ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1235
|
+
"Attempt: ",
|
|
1236
|
+
progress.attemptId
|
|
1237
|
+
] }) : null,
|
|
1238
|
+
progress?.observedState ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1239
|
+
"Zerodha state: ",
|
|
1240
|
+
progress.observedState
|
|
1241
|
+
] }) : null,
|
|
1242
|
+
progress?.failedOtpAttempts !== void 0 ? /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1243
|
+
"Failed OTP attempts: ",
|
|
1244
|
+
progress.failedOtpAttempts
|
|
1245
|
+
] }) : null,
|
|
937
1246
|
error ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }) : null
|
|
938
1247
|
] });
|
|
939
1248
|
}
|
|
@@ -941,6 +1250,7 @@ var init_app = __esm({
|
|
|
941
1250
|
"src/app.tsx"() {
|
|
942
1251
|
"use strict";
|
|
943
1252
|
init_gate_api();
|
|
1253
|
+
init_zerodha_actions();
|
|
944
1254
|
}
|
|
945
1255
|
});
|
|
946
1256
|
|
|
@@ -1492,9 +1802,9 @@ function sessionHasCookies(session) {
|
|
|
1492
1802
|
return Boolean(session.operatorSessionCookie || session.clientIdentityCookie);
|
|
1493
1803
|
}
|
|
1494
1804
|
var CliProfileStore = class {
|
|
1495
|
-
constructor(homeDirectory = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd(), lockWaitMs = LOCK_WAIT_MS) {
|
|
1805
|
+
constructor(homeDirectory = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd(), lockWaitMs = LOCK_WAIT_MS, directoryName = ".lmt") {
|
|
1496
1806
|
this.lockWaitMs = lockWaitMs;
|
|
1497
|
-
this.directory = path2.join(homeDirectory,
|
|
1807
|
+
this.directory = path2.join(homeDirectory, directoryName);
|
|
1498
1808
|
this.filePath = path2.join(this.directory, FILE_NAME);
|
|
1499
1809
|
this.lockPath = path2.join(this.directory, `${FILE_NAME}.lock`);
|
|
1500
1810
|
}
|
|
@@ -1722,45 +2032,161 @@ var CliProfileStore = class {
|
|
|
1722
2032
|
}
|
|
1723
2033
|
};
|
|
1724
2034
|
|
|
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
|
-
|
|
2035
|
+
// src/zerodha-validation.ts
|
|
2036
|
+
init_action_deadline();
|
|
2037
|
+
init_gate_api();
|
|
2038
|
+
init_profile_name();
|
|
2039
|
+
|
|
2040
|
+
// src/protected-input.ts
|
|
2041
|
+
var ProtectedInputError = class extends Error {
|
|
2042
|
+
code = "invalid_invocation";
|
|
2043
|
+
};
|
|
2044
|
+
async function readProtectedOtp(stdin, signal) {
|
|
2045
|
+
if (stdin.isTTY) throw new ProtectedInputError("OTP requires piped protected stdin; interactive mode provides masked entry.");
|
|
2046
|
+
const chunks = [];
|
|
2047
|
+
let size = 0;
|
|
2048
|
+
try {
|
|
2049
|
+
const iterator = stdin[Symbol.asyncIterator]();
|
|
2050
|
+
while (true) {
|
|
2051
|
+
if (signal.aborted) throw new Error("The action exceeded its deadline.");
|
|
2052
|
+
const next = await new Promise((resolve, reject) => {
|
|
2053
|
+
const abort = () => {
|
|
2054
|
+
stdin.destroy?.();
|
|
2055
|
+
reject(new Error("The action exceeded its deadline."));
|
|
2056
|
+
};
|
|
2057
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
2058
|
+
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));
|
|
2059
|
+
});
|
|
2060
|
+
if (next.done) break;
|
|
2061
|
+
size += typeof next.value === "string" ? Buffer.byteLength(next.value) : next.value.byteLength;
|
|
2062
|
+
if (size > 8) {
|
|
2063
|
+
stdin.destroy?.();
|
|
2064
|
+
throw new ProtectedInputError("OTP input is too large; provide one six-digit line.");
|
|
2065
|
+
}
|
|
2066
|
+
chunks.push(Buffer.from(next.value));
|
|
1756
2067
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2068
|
+
const buffer = Buffer.concat(chunks);
|
|
2069
|
+
try {
|
|
2070
|
+
const value = buffer.toString("utf8");
|
|
2071
|
+
if (!/^\d{6}(?:\r?\n)?$/.test(value)) throw new ProtectedInputError("OTP input must be one six-digit line.");
|
|
2072
|
+
return value.slice(0, 6);
|
|
2073
|
+
} finally {
|
|
2074
|
+
buffer.fill(0);
|
|
2075
|
+
}
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
if (signal.aborted || error instanceof ProtectedInputError) throw error;
|
|
2078
|
+
throw new ProtectedInputError("Could not read OTP from protected stdin; provide one six-digit line.");
|
|
2079
|
+
} finally {
|
|
2080
|
+
for (const chunk of chunks) chunk.fill(0);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// src/zerodha-validation.ts
|
|
2085
|
+
init_zerodha_actions();
|
|
2086
|
+
async function runZerodhaValidation(options) {
|
|
2087
|
+
const started = performance.now();
|
|
2088
|
+
const controller = new AbortController();
|
|
2089
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 12e4);
|
|
2090
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
2091
|
+
const observations = [];
|
|
2092
|
+
let api;
|
|
2093
|
+
let otp;
|
|
2094
|
+
let result = {
|
|
2095
|
+
scenario: "zerodha-session",
|
|
2096
|
+
outcome: "fail",
|
|
2097
|
+
reason: "validation_action_failed",
|
|
2098
|
+
exitCode: 1,
|
|
2099
|
+
target: { environment: options.target.environment, origin: options.target.origin },
|
|
2100
|
+
profile: options.callerProfile,
|
|
2101
|
+
...options.attemptId ? { attemptId: options.attemptId } : {},
|
|
2102
|
+
observations,
|
|
2103
|
+
nextStep: null
|
|
2104
|
+
};
|
|
2105
|
+
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]");
|
|
2106
|
+
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.";
|
|
2107
|
+
try {
|
|
2108
|
+
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" });
|
|
2109
|
+
try {
|
|
2110
|
+
assertClientProfileName(options.callerProfile);
|
|
2111
|
+
} catch {
|
|
2112
|
+
throw new ProtectedInputError(CLIENT_PROFILE_NAME_ERROR);
|
|
2113
|
+
}
|
|
2114
|
+
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.");
|
|
2115
|
+
if (options.readOtp && !options.attemptId) throw new ProtectedInputError("OTP submission requires an explicit --attempt ID from a prior login or validation.");
|
|
2116
|
+
const actions = createOperatorSessionActions({
|
|
2117
|
+
target: options.target,
|
|
2118
|
+
profile: options.callerProfile,
|
|
2119
|
+
store: options.store,
|
|
2120
|
+
timeoutMs: options.timeoutMs ?? 12e4,
|
|
2121
|
+
signal,
|
|
2122
|
+
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
2123
|
+
});
|
|
2124
|
+
api = await withinActionDeadline(actions.getApi(), signal, "The Zerodha Session validation exceeded its deadline.");
|
|
2125
|
+
const operator = await actions.status();
|
|
2126
|
+
result.observedOperator = operator.observedOperator;
|
|
2127
|
+
if (options.readOtp) {
|
|
2128
|
+
otp = await withinActionDeadline(options.readOtp(signal), signal, "The Zerodha Session validation exceeded its deadline.");
|
|
2129
|
+
if (!/^\d{6}$/.test(otp)) throw new ProtectedInputError("OTP input must be one six-digit line.");
|
|
2130
|
+
}
|
|
2131
|
+
const action = options.attemptId ? options.readOtp ? "otp" : "observe" : "login";
|
|
2132
|
+
const login = await runZerodhaAction(api, action, options.target.operatorId, signal, options.attemptId, otp, {
|
|
2133
|
+
onObservation(event) {
|
|
2134
|
+
result.attemptId = event.attemptId;
|
|
2135
|
+
if (event.type === "status") {
|
|
2136
|
+
result.observedState = event.status;
|
|
2137
|
+
result.observedOperator = event.operatorId;
|
|
2138
|
+
observations.push(event.status);
|
|
2139
|
+
if (event.failedOtpAttempts !== void 0) result.failedOtpAttempts = event.failedOtpAttempts;
|
|
2140
|
+
if (event.message) result.message = safeText(event.message);
|
|
2141
|
+
}
|
|
2142
|
+
options.onObservation?.(event);
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
result.attemptId = login.attemptId;
|
|
2146
|
+
if (login.status === "awaiting_otp") {
|
|
2147
|
+
const rejected = !!options.readOtp && (login.failedOtpAttempts ?? 0) > 0;
|
|
2148
|
+
result = { ...result, outcome: rejected ? "fail" : "blocked", reason: rejected ? "otp_rejected" : "awaiting_otp", exitCode: rejected ? 1 : 4, nextStep: resumeStep() };
|
|
2149
|
+
} else {
|
|
2150
|
+
result = { ...result, outcome: "pass", reason: "zerodha_session_validated", exitCode: 0, nextStep: null };
|
|
2151
|
+
}
|
|
2152
|
+
} catch (cause) {
|
|
2153
|
+
const error = cause instanceof Error ? cause : void 0;
|
|
2154
|
+
if (error?.attemptId) result.attemptId = error.attemptId;
|
|
2155
|
+
if (error?.observedState) result.observedState = error.observedState;
|
|
2156
|
+
if (error?.observedOperator) result.observedOperator = error.observedOperator;
|
|
2157
|
+
const code = error?.code;
|
|
2158
|
+
if (signal.aborted) {
|
|
2159
|
+
result.reason = "deadline_exceeded";
|
|
2160
|
+
result.message = "The Zerodha Session validation exceeded its deadline.";
|
|
2161
|
+
} else if (cause instanceof OperatorSessionRejectedError) {
|
|
2162
|
+
result.outcome = "blocked";
|
|
2163
|
+
result.reason = "operator_login_required";
|
|
2164
|
+
result.exitCode = 3;
|
|
2165
|
+
result.message = "No valid Operator Session is available; run login with this environment and profile first.";
|
|
2166
|
+
} else if (code && ["zerodha_credentials_missing", "secret_store_unavailable", "operator_not_configured"].includes(code)) {
|
|
2167
|
+
result.outcome = "blocked";
|
|
2168
|
+
result.reason = code;
|
|
2169
|
+
result.exitCode = 4;
|
|
2170
|
+
result.message = safeText(error.message);
|
|
2171
|
+
} else {
|
|
2172
|
+
result.reason = cause instanceof ProtectedInputError ? "invalid_invocation" : cause instanceof ZerodhaLoginAttemptUnavailableError ? "login_attempt_inactive" : cause instanceof ZerodhaLoginFailedError ? "zerodha_login_failed" : code ?? "validation_action_failed";
|
|
2173
|
+
result.exitCode = cause instanceof ProtectedInputError || code === "invalid_target" ? 2 : 1;
|
|
2174
|
+
result.message = error ? safeText(error.message) : "The Zerodha Session validation failed.";
|
|
2175
|
+
}
|
|
2176
|
+
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();
|
|
2177
|
+
if (cause instanceof OperatorSessionRejectedError && api && !signal.aborted) {
|
|
2178
|
+
await options.store.saveSession(options.callerProfile, options.target, api.getCookies()).catch(() => void 0);
|
|
2179
|
+
}
|
|
2180
|
+
} finally {
|
|
2181
|
+
otp = void 0;
|
|
2182
|
+
clearTimeout(timer);
|
|
2183
|
+
}
|
|
2184
|
+
const metadata = api?.responseMetadata();
|
|
2185
|
+
return { ...result, elapsedMs: Math.round(performance.now() - started), requestId: metadata?.requestId ?? null, buildId: metadata?.buildId ?? null };
|
|
1761
2186
|
}
|
|
1762
2187
|
|
|
1763
2188
|
// src/commands.ts
|
|
2189
|
+
init_zerodha_actions();
|
|
1764
2190
|
init_gate_api();
|
|
1765
2191
|
|
|
1766
2192
|
// src/validation-scenario.ts
|
|
@@ -2025,17 +2451,19 @@ function parseTimeout(raw) {
|
|
|
2025
2451
|
return Math.ceil(value);
|
|
2026
2452
|
}
|
|
2027
2453
|
function splitHeadlessArgv(argv) {
|
|
2454
|
+
if (hasArgumentOtp(argv)) throw new InvocationError("--otp arguments are no longer accepted. Use --otp-stdin with an explicit --attempt ID.");
|
|
2028
2455
|
if (argv.length === 0 || argv[0] === "interactive") {
|
|
2029
2456
|
throw new InvocationError("Choose a headless command or run `live-market-terminal interactive`.");
|
|
2030
2457
|
}
|
|
2031
2458
|
const options = /* @__PURE__ */ new Map();
|
|
2032
2459
|
let credentialStdin = false;
|
|
2460
|
+
let otpStdin = false;
|
|
2033
2461
|
const positional = [];
|
|
2034
2462
|
for (let index = 0; index < argv.length; index += 1) {
|
|
2035
2463
|
const token = argv[index];
|
|
2036
|
-
if (token
|
|
2037
|
-
if (
|
|
2038
|
-
|
|
2464
|
+
if (token === "--otp-stdin") {
|
|
2465
|
+
if (otpStdin) throw new InvocationError("Argument --otp-stdin may be specified only once.");
|
|
2466
|
+
otpStdin = true;
|
|
2039
2467
|
continue;
|
|
2040
2468
|
}
|
|
2041
2469
|
if (token === "--credential-stdin") {
|
|
@@ -2043,7 +2471,7 @@ function splitHeadlessArgv(argv) {
|
|
|
2043
2471
|
credentialStdin = true;
|
|
2044
2472
|
continue;
|
|
2045
2473
|
}
|
|
2046
|
-
if (token === "--env" || token === "--profile" || token === "--timeout" || token === "--attempt" || token === "--
|
|
2474
|
+
if (token === "--env" || token === "--profile" || token === "--timeout" || token === "--attempt" || token === "--date") {
|
|
2047
2475
|
const value = argv[index + 1];
|
|
2048
2476
|
if (value === void 0 || value.startsWith("--")) {
|
|
2049
2477
|
throw new InvocationError(`Invalid or incomplete command argument: ${token}`);
|
|
@@ -2058,10 +2486,32 @@ function splitHeadlessArgv(argv) {
|
|
|
2058
2486
|
}
|
|
2059
2487
|
positional.push(token);
|
|
2060
2488
|
}
|
|
2061
|
-
return { positional, options, credentialStdin };
|
|
2489
|
+
return { positional, options, credentialStdin, otpStdin };
|
|
2062
2490
|
}
|
|
2063
2491
|
function parseArguments(argv) {
|
|
2064
|
-
const { positional, options, credentialStdin } = splitHeadlessArgv(argv);
|
|
2492
|
+
const { positional, options, credentialStdin, otpStdin } = splitHeadlessArgv(argv);
|
|
2493
|
+
if (positional[0] === "candle-export") {
|
|
2494
|
+
const instrument = positional[1]?.match(/^([A-Za-z][A-Za-z0-9]{0,31}):([^:\s]{1,100})$/);
|
|
2495
|
+
const date = options.get("--date");
|
|
2496
|
+
const parsedDate = date ? /* @__PURE__ */ new Date(`${date}T00:00:00Z`) : void 0;
|
|
2497
|
+
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")) {
|
|
2498
|
+
throw new InvocationError("Use candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD].");
|
|
2499
|
+
}
|
|
2500
|
+
const environment2 = options.get("--env");
|
|
2501
|
+
const profile2 = options.get("--profile");
|
|
2502
|
+
if (!environment2 || !profile2) throw new InvocationError("Headless commands require both --env local|dev and --profile NAME.");
|
|
2503
|
+
validateCommandProfile(profile2);
|
|
2504
|
+
return {
|
|
2505
|
+
kind: "candle-export",
|
|
2506
|
+
exchange: instrument[1].toUpperCase(),
|
|
2507
|
+
tradingSymbol: instrument[2].toUpperCase(),
|
|
2508
|
+
environment: environment2,
|
|
2509
|
+
profile: profile2,
|
|
2510
|
+
...date ? { date } : {},
|
|
2511
|
+
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
if (options.has("--date")) throw new InvocationError("--date is only supported for candle-export.");
|
|
2065
2515
|
if (positional[0] === "zerodha") {
|
|
2066
2516
|
const action = positional[1];
|
|
2067
2517
|
if (!["login", "observe", "otp", "status"].includes(action) || positional.length !== 2 || credentialStdin) {
|
|
@@ -2078,22 +2528,38 @@ function parseArguments(argv) {
|
|
|
2078
2528
|
if ((action === "observe" || action === "otp") && !attemptId) {
|
|
2079
2529
|
throw new InvocationError("--attempt ID is required for zerodha observe and otp. Use the attemptId returned by zerodha login.");
|
|
2080
2530
|
}
|
|
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.");
|
|
2531
|
+
if (action === "otp" && !otpStdin || otpStdin && action !== "otp") {
|
|
2532
|
+
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
2533
|
}
|
|
2085
|
-
if (otp !== void 0 && !/^\d{6}$/.test(otp)) throw new InvocationError("OTP must be six digits.");
|
|
2086
2534
|
return {
|
|
2087
2535
|
kind: "zerodha",
|
|
2088
2536
|
action,
|
|
2089
2537
|
environment: environment2,
|
|
2090
2538
|
profile: profile2,
|
|
2091
2539
|
attemptId,
|
|
2092
|
-
|
|
2540
|
+
otpStdin,
|
|
2093
2541
|
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2094
2542
|
};
|
|
2095
2543
|
}
|
|
2096
|
-
if (
|
|
2544
|
+
if (positional[0] === "validate" && positional[1] === "zerodha-session") {
|
|
2545
|
+
if (positional.length !== 2 || credentialStdin) throw new InvocationError("Use validate zerodha-session with optional --attempt ID and --otp-stdin.");
|
|
2546
|
+
const environment2 = options.get("--env");
|
|
2547
|
+
const profile2 = options.get("--profile");
|
|
2548
|
+
if (!environment2 || !profile2) throw new InvocationError("Headless commands require both --env local|dev and --profile NAME.");
|
|
2549
|
+
validateCommandProfile(profile2);
|
|
2550
|
+
const attemptId = options.get("--attempt");
|
|
2551
|
+
if (attemptId !== void 0 && !/^[a-zA-Z0-9_-]{1,200}$/.test(attemptId)) throw new InvocationError("--attempt must be a valid Zerodha attempt ID.");
|
|
2552
|
+
if (otpStdin && !attemptId) throw new InvocationError("--otp-stdin requires an explicit --attempt ID from a prior login or validation.");
|
|
2553
|
+
return {
|
|
2554
|
+
kind: "validate-zerodha",
|
|
2555
|
+
environment: environment2,
|
|
2556
|
+
profile: profile2,
|
|
2557
|
+
attemptId,
|
|
2558
|
+
otpStdin,
|
|
2559
|
+
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
if (otpStdin || options.has("--attempt")) throw new InvocationError("--otp-stdin and --attempt are Zerodha options only.");
|
|
2097
2563
|
if (positional[0] === "credential") {
|
|
2098
2564
|
if (positional[1] !== "provision" || positional.length > 2) {
|
|
2099
2565
|
throw new InvocationError("Use `credential provision` to provision the protected SMS credential.");
|
|
@@ -2119,9 +2585,9 @@ function parseArguments(argv) {
|
|
|
2119
2585
|
throw new InvocationError("Unexpected positional argument; use --help for command syntax.");
|
|
2120
2586
|
}
|
|
2121
2587
|
} else if (positional.length === 0) {
|
|
2122
|
-
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session.");
|
|
2588
|
+
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session|zerodha-session.");
|
|
2123
2589
|
} else {
|
|
2124
|
-
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session.");
|
|
2590
|
+
throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session|zerodha-session.");
|
|
2125
2591
|
}
|
|
2126
2592
|
const environment = options.get("--env");
|
|
2127
2593
|
const profile = options.get("--profile");
|
|
@@ -2138,11 +2604,11 @@ function validateCommandProfile(profile) {
|
|
|
2138
2604
|
}
|
|
2139
2605
|
}
|
|
2140
2606
|
function durationFor(command) {
|
|
2141
|
-
return command.kind === "validate" ? command.timeoutMs ?? 12e4 : command.kind === "credential-provision" ? 3e4 : command.timeoutMs ?? 3e4;
|
|
2607
|
+
return command.kind === "validate" || command.kind === "validate-zerodha" ? command.timeoutMs ?? 12e4 : command.kind === "credential-provision" ? 3e4 : command.timeoutMs ?? 3e4;
|
|
2142
2608
|
}
|
|
2143
2609
|
function errorDetails(error, signal) {
|
|
2144
2610
|
if (signal?.aborted) return { code: "deadline_exceeded", message: "The action exceeded its deadline.", exitCode: 1 };
|
|
2145
|
-
if (error instanceof InvocationError) {
|
|
2611
|
+
if (error instanceof InvocationError || error instanceof ProtectedInputError) {
|
|
2146
2612
|
return { code: error.code, message: error.message, exitCode: 2 };
|
|
2147
2613
|
}
|
|
2148
2614
|
if (error instanceof OperatorSessionRejectedError) {
|
|
@@ -2151,6 +2617,11 @@ function errorDetails(error, signal) {
|
|
|
2151
2617
|
if (error instanceof OperatorCredentialsRejectedError) {
|
|
2152
2618
|
return { code: "incorrect_credentials", message: "Gate rejected the Operator Login credentials.", exitCode: 1 };
|
|
2153
2619
|
}
|
|
2620
|
+
if (error instanceof ZerodhaLoginAttemptUnavailableError) return { code: "login_attempt_inactive", message: error.message, exitCode: 1 };
|
|
2621
|
+
if (error instanceof ZerodhaLoginFailedError) return { code: "zerodha_login_failed", message: error.message, exitCode: 1 };
|
|
2622
|
+
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)) {
|
|
2623
|
+
return { code: error.code, message: error.message, exitCode: 1 };
|
|
2624
|
+
}
|
|
2154
2625
|
if (error instanceof SmsReadError) {
|
|
2155
2626
|
return { code: error.code, message: error.message, exitCode: 1 };
|
|
2156
2627
|
}
|
|
@@ -2196,18 +2667,25 @@ async function readProtectedInput(stdin, signal, label = "SMS credential") {
|
|
|
2196
2667
|
return credential;
|
|
2197
2668
|
}
|
|
2198
2669
|
async function runHeadlessCli(argv, dependencies = {}) {
|
|
2199
|
-
|
|
2670
|
+
let otp;
|
|
2671
|
+
const protect = (stream) => ({ write(chunk) {
|
|
2672
|
+
return stream.write(otp ? chunk.replaceAll(otp, "[redacted]") : chunk);
|
|
2673
|
+
} });
|
|
2674
|
+
const streams = { stdout: protect(dependencies.stdout ?? defaultOut), stderr: protect(dependencies.stderr ?? defaultErr) };
|
|
2200
2675
|
const started = performance.now();
|
|
2201
2676
|
let command;
|
|
2202
2677
|
let target;
|
|
2203
2678
|
let profile;
|
|
2204
2679
|
let actionApi;
|
|
2205
2680
|
let signal;
|
|
2681
|
+
let deadlineTimer;
|
|
2206
2682
|
let store;
|
|
2207
2683
|
try {
|
|
2208
2684
|
command = parseArguments(argv);
|
|
2209
2685
|
const timeoutMs = durationFor(command);
|
|
2210
|
-
|
|
2686
|
+
const deadline = new AbortController();
|
|
2687
|
+
deadlineTimer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
2688
|
+
signal = deadline.signal;
|
|
2211
2689
|
profile = command.profile;
|
|
2212
2690
|
try {
|
|
2213
2691
|
target = resolveCliTarget({ LMT_ENV: command.environment }, dependencies.configPath);
|
|
@@ -2230,6 +2708,33 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2230
2708
|
}, streams);
|
|
2231
2709
|
return 0;
|
|
2232
2710
|
}
|
|
2711
|
+
if (command.kind === "validate-zerodha") {
|
|
2712
|
+
const validation = await runZerodhaValidation({
|
|
2713
|
+
target,
|
|
2714
|
+
callerProfile: profile,
|
|
2715
|
+
store,
|
|
2716
|
+
timeoutMs,
|
|
2717
|
+
signal,
|
|
2718
|
+
attemptId: command.attemptId,
|
|
2719
|
+
...command.otpStdin ? { readOtp: (inputSignal) => readProtectedOtp(dependencies.stdin ?? process.stdin, inputSignal) } : {},
|
|
2720
|
+
...dependencies.fetchImpl ? { fetchImpl: dependencies.fetchImpl } : {}
|
|
2721
|
+
});
|
|
2722
|
+
emit({
|
|
2723
|
+
version: 1,
|
|
2724
|
+
command: "validate zerodha-session",
|
|
2725
|
+
outcome: validation.outcome,
|
|
2726
|
+
reason: validation.reason,
|
|
2727
|
+
target: validation.target,
|
|
2728
|
+
profile,
|
|
2729
|
+
observedOperator: validation.observedOperator,
|
|
2730
|
+
attemptId: validation.attemptId,
|
|
2731
|
+
observedState: validation.observedState,
|
|
2732
|
+
elapsedMs: validation.elapsedMs,
|
|
2733
|
+
metadata: { requestId: validation.requestId, buildId: validation.buildId },
|
|
2734
|
+
validation
|
|
2735
|
+
}, streams, validation.exitCode === 0 ? void 0 : `Zerodha Session validation ${validation.outcome}: ${validation.reason}. ${validation.nextStep ?? ""}`);
|
|
2736
|
+
return validation.exitCode;
|
|
2737
|
+
}
|
|
2233
2738
|
if (command.kind === "validate") {
|
|
2234
2739
|
const validation = await runOperatorSessionValidation({
|
|
2235
2740
|
target,
|
|
@@ -2267,15 +2772,36 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2267
2772
|
...dependencies.readLoginCredentials ? { readLoginCredentials: dependencies.readLoginCredentials } : {}
|
|
2268
2773
|
});
|
|
2269
2774
|
actionApi = await actions.getApi();
|
|
2775
|
+
if (command.kind === "candle-export") {
|
|
2776
|
+
await actions.status();
|
|
2777
|
+
const candleExport = await actionApi.exportCandles({
|
|
2778
|
+
exchange: command.exchange,
|
|
2779
|
+
tradingSymbol: command.tradingSymbol,
|
|
2780
|
+
...command.date ? { date: command.date } : {}
|
|
2781
|
+
});
|
|
2782
|
+
emit({
|
|
2783
|
+
version: 1,
|
|
2784
|
+
command: command.kind,
|
|
2785
|
+
outcome: "success",
|
|
2786
|
+
target: { environment: target.environment, origin: target.origin },
|
|
2787
|
+
profile,
|
|
2788
|
+
observedOperator: target.operatorId,
|
|
2789
|
+
candleExport,
|
|
2790
|
+
elapsedMs: Math.round(performance.now() - started),
|
|
2791
|
+
metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
|
|
2792
|
+
}, streams);
|
|
2793
|
+
return 0;
|
|
2794
|
+
}
|
|
2270
2795
|
if (command.kind === "zerodha") {
|
|
2271
2796
|
await actions.status();
|
|
2797
|
+
if (command.otpStdin) otp = await readProtectedOtp(dependencies.stdin ?? process.stdin, signal);
|
|
2272
2798
|
const zerodha = await runZerodhaAction(
|
|
2273
2799
|
actionApi,
|
|
2274
2800
|
command.action,
|
|
2275
2801
|
target.operatorId,
|
|
2276
2802
|
signal,
|
|
2277
2803
|
command.attemptId,
|
|
2278
|
-
|
|
2804
|
+
otp
|
|
2279
2805
|
);
|
|
2280
2806
|
const awaitingOtp = zerodha.status === "awaiting_otp";
|
|
2281
2807
|
emit({
|
|
@@ -2330,13 +2856,18 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2330
2856
|
await store.saveSession(profile, target, actionApi.getCookies()).catch(() => void 0);
|
|
2331
2857
|
}
|
|
2332
2858
|
const details = errorDetails(error, signal);
|
|
2859
|
+
const recovery = error instanceof Error ? error : void 0;
|
|
2860
|
+
const attemptId = recovery?.attemptId ?? (command?.kind === "zerodha" ? command.attemptId : void 0);
|
|
2333
2861
|
emit({
|
|
2334
2862
|
version: 1,
|
|
2335
|
-
command: command?.kind === "zerodha" ? `zerodha ${command.action}` : command?.kind === "credential-provision" ? "credential provision" : command?.kind === "validate" ? "validate operator-session" : command?.kind ?? "",
|
|
2863
|
+
command: command?.kind === "zerodha" ? `zerodha ${command.action}` : command?.kind === "credential-provision" ? "credential provision" : command?.kind === "validate-zerodha" ? "validate zerodha-session" : command?.kind === "validate" ? "validate operator-session" : command?.kind ?? "",
|
|
2336
2864
|
outcome: details.exitCode === 4 ? "blocked" : "fail",
|
|
2337
2865
|
reason: details.code,
|
|
2338
2866
|
...target ? { target: { environment: target.environment, origin: target.origin } } : command ? { target: { environment: command.environment } } : {},
|
|
2339
2867
|
...profile ? { profile } : {},
|
|
2868
|
+
...attemptId ? { attemptId, recovery: recovery?.recovery ?? "Observe this exact attempt with the same target and profile; start explicitly only if Gate reports it inactive." } : {},
|
|
2869
|
+
...recovery?.observedState ? { observedState: recovery.observedState } : {},
|
|
2870
|
+
...recovery?.observedOperator ? { observedOperator: recovery.observedOperator } : {},
|
|
2340
2871
|
elapsedMs: Math.round(performance.now() - started),
|
|
2341
2872
|
metadata: {
|
|
2342
2873
|
requestId: actionApi?.responseMetadata().requestId ?? null,
|
|
@@ -2345,8 +2876,14 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2345
2876
|
error: { code: details.code, message: details.message }
|
|
2346
2877
|
}, streams, details.exitCode === 2 ? `Invalid CLI invocation: ${details.message}` : details.message);
|
|
2347
2878
|
return details.exitCode;
|
|
2879
|
+
} finally {
|
|
2880
|
+
otp = void 0;
|
|
2881
|
+
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
2348
2882
|
}
|
|
2349
2883
|
}
|
|
2884
|
+
function hasArgumentOtp(argv) {
|
|
2885
|
+
return argv.some((token) => token === "--otp" || token.startsWith("--otp="));
|
|
2886
|
+
}
|
|
2350
2887
|
|
|
2351
2888
|
// src/index.ts
|
|
2352
2889
|
init_profile_name();
|
|
@@ -2368,26 +2905,36 @@ Usage (lmt is an alias of live-market-terminal):
|
|
|
2368
2905
|
live-market-terminal interactive credential provision [--env local|dev] [--profile NAME]
|
|
2369
2906
|
live-market-terminal login --env local|dev --profile NAME [--timeout DURATION]
|
|
2370
2907
|
live-market-terminal logout --env local|dev --profile NAME [--timeout DURATION]
|
|
2908
|
+
live-market-terminal candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD] --env local|dev --profile NAME [--timeout DURATION]
|
|
2371
2909
|
live-market-terminal status --env local|dev --profile NAME [--timeout DURATION]
|
|
2372
2910
|
live-market-terminal zerodha login --env local|dev --profile NAME [--timeout DURATION]
|
|
2373
2911
|
live-market-terminal zerodha observe --attempt ID --env local|dev --profile NAME [--timeout DURATION]
|
|
2374
|
-
live-market-terminal zerodha otp --attempt ID --otp
|
|
2912
|
+
live-market-terminal zerodha otp --attempt ID --otp-stdin --env local|dev --profile NAME [--timeout DURATION]
|
|
2375
2913
|
live-market-terminal zerodha status --env local|dev --profile NAME [--timeout DURATION]
|
|
2376
2914
|
live-market-terminal validate operator-session --env local|dev --profile NAME [--timeout DURATION]
|
|
2915
|
+
live-market-terminal validate zerodha-session --env local|dev --profile NAME [--attempt ID [--otp-stdin]] [--timeout DURATION]
|
|
2377
2916
|
live-market-terminal credential provision --env local|dev --profile NAME --credential-stdin
|
|
2378
2917
|
|
|
2379
|
-
Headless options (--env, --profile, --timeout, --attempt, --otp, --credential-stdin) may appear before or after the command.
|
|
2918
|
+
Headless options (--env, --profile, --timeout, --attempt, --otp-stdin, --credential-stdin) may appear before or after the command.
|
|
2380
2919
|
|
|
2381
2920
|
From the monorepo root:
|
|
2382
2921
|
pnpm cli [--profile NAME] # interactive; --env defaults to local
|
|
2383
2922
|
pnpm cli --env dev --profile my-dev-session
|
|
2384
2923
|
pnpm -s cli:headless status --env local --profile default # build step runs quietly (-s)
|
|
2385
2924
|
|
|
2925
|
+
Candle Export publishes a new immutable workbook link and returns its context in candleExport JSON.
|
|
2926
|
+
Empty or unavailable sources are explained in the workbook. Export never starts Zerodha login or token recovery.
|
|
2927
|
+
|
|
2386
2928
|
Targets: local and dev only. Production is not permitted.
|
|
2387
2929
|
|
|
2388
|
-
Zerodha login is two steps: zerodha login, then zerodha otp --attempt ID --otp
|
|
2930
|
+
Zerodha login is two steps: zerodha login, then zerodha otp --attempt ID --otp-stdin.
|
|
2389
2931
|
Obtain the OTP from Kite after login returns awaiting_otp; expired attempts require a new login.
|
|
2932
|
+
OTP arguments (--otp CODE and --otp=CODE) are rejected. Pipe protected input; headless mode never prompts.
|
|
2390
2933
|
|
|
2934
|
+
Validate Zerodha Session starts a new attempt; resume the returned ID with --attempt ID.
|
|
2935
|
+
Supply --otp-stdin only when submitting a human OTP to that exact attempt.
|
|
2936
|
+
Validation retains the caller profile session and can replace Broker's active attempt or refresh its shared token.
|
|
2937
|
+
Interactive mode offers Validate Zerodha Session and explicit Resume Zerodha Validation.
|
|
2391
2938
|
Interactive mode restores a saved Operator Session when Gate still accepts it.
|
|
2392
2939
|
Headless commands write one JSON object per line to stdout; diagnostics go to stderr.
|
|
2393
2940
|
|
|
@@ -2402,8 +2949,9 @@ Options:
|
|
|
2402
2949
|
--env local|dev Target environment (interactive default: local; headless: required)
|
|
2403
2950
|
--profile NAME Client profile (default: default for interactive)
|
|
2404
2951
|
--timeout DURATION Action deadline, e.g. 45s, 2m, 30000ms (headless)
|
|
2952
|
+
--date YYYY-MM-DD Single IST date for candle-export; omitted: latest completed exchange session
|
|
2405
2953
|
--attempt ID Attempt ID returned by zerodha login (required for observe and otp)
|
|
2406
|
-
--otp
|
|
2954
|
+
--otp-stdin Read one six-digit OTP line from piped protected stdin (maximum 8 bytes)
|
|
2407
2955
|
--credential-stdin Read SMS credential from piped stdin (headless provision)
|
|
2408
2956
|
--help, -h Show this help
|
|
2409
2957
|
|
|
@@ -2423,6 +2971,10 @@ async function bootstrapCli(dependencies = {}) {
|
|
|
2423
2971
|
return;
|
|
2424
2972
|
}
|
|
2425
2973
|
const argv = normalizeArgv(process.argv.slice(2));
|
|
2974
|
+
if (hasArgumentOtp(argv)) {
|
|
2975
|
+
process.exitCode = await runHeadlessCli(argv);
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2426
2978
|
if (wantsCliHelp(argv)) {
|
|
2427
2979
|
process.stdout.write(formatCliHelp());
|
|
2428
2980
|
return;
|
|
@@ -2477,6 +3029,7 @@ async function bootstrapCli(dependencies = {}) {
|
|
|
2477
3029
|
};
|
|
2478
3030
|
const app = render(createElement(LoginApp2, {
|
|
2479
3031
|
api,
|
|
3032
|
+
expectedOperatorId: target.operatorId,
|
|
2480
3033
|
actionTimeoutMs: 3e4,
|
|
2481
3034
|
async onResumeSession() {
|
|
2482
3035
|
return actions.resumeInteractiveSession();
|
|
@@ -2496,6 +3049,16 @@ async function bootstrapCli(dependencies = {}) {
|
|
|
2496
3049
|
});
|
|
2497
3050
|
return result;
|
|
2498
3051
|
},
|
|
3052
|
+
onValidateZerodha: (input) => runZerodhaValidation({
|
|
3053
|
+
target,
|
|
3054
|
+
callerProfile: profile,
|
|
3055
|
+
store,
|
|
3056
|
+
timeoutMs: 12e4,
|
|
3057
|
+
signal: input.signal,
|
|
3058
|
+
attemptId: input.attemptId,
|
|
3059
|
+
onObservation: input.onObservation,
|
|
3060
|
+
...input.otp ? { readOtp: async () => input.otp } : {}
|
|
3061
|
+
}),
|
|
2499
3062
|
onLogout: () => actions.logout()
|
|
2500
3063
|
}));
|
|
2501
3064
|
await app.waitUntilExit();
|