@ai-digital-factory/lmt-cli 0.1.1 → 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 +45 -18
- package/dist/index.js +711 -199
- 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
|
|
|
@@ -1441,8 +1751,7 @@ function normalizeSession(value) {
|
|
|
1441
1751
|
operatorId: session.operatorId,
|
|
1442
1752
|
updatedAt: session.updatedAt,
|
|
1443
1753
|
...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
|
|
1444
|
-
...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {}
|
|
1445
|
-
...typeof session.zerodhaAttemptId === "string" && /^[a-zA-Z0-9_-]{1,200}$/.test(session.zerodhaAttemptId) ? { zerodhaAttemptId: session.zerodhaAttemptId } : {}
|
|
1754
|
+
...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {}
|
|
1446
1755
|
};
|
|
1447
1756
|
}
|
|
1448
1757
|
function parseRecord(raw) {
|
|
@@ -1493,9 +1802,9 @@ function sessionHasCookies(session) {
|
|
|
1493
1802
|
return Boolean(session.operatorSessionCookie || session.clientIdentityCookie);
|
|
1494
1803
|
}
|
|
1495
1804
|
var CliProfileStore = class {
|
|
1496
|
-
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") {
|
|
1497
1806
|
this.lockWaitMs = lockWaitMs;
|
|
1498
|
-
this.directory = path2.join(homeDirectory,
|
|
1807
|
+
this.directory = path2.join(homeDirectory, directoryName);
|
|
1499
1808
|
this.filePath = path2.join(this.directory, FILE_NAME);
|
|
1500
1809
|
this.lockPath = path2.join(this.directory, `${FILE_NAME}.lock`);
|
|
1501
1810
|
}
|
|
@@ -1539,10 +1848,6 @@ var CliProfileStore = class {
|
|
|
1539
1848
|
...cookies.clientIdentityCookie ? { clientIdentityCookie: cookies.clientIdentityCookie } : {},
|
|
1540
1849
|
...cookies.operatorSessionCookie ? { operatorSessionCookie: cookies.operatorSessionCookie } : {}
|
|
1541
1850
|
};
|
|
1542
|
-
const previous = profileRecord.sessions[index];
|
|
1543
|
-
if (previous?.operatorSessionCookie && previous.operatorSessionCookie === next.operatorSessionCookie && previous.zerodhaAttemptId) {
|
|
1544
|
-
next.zerodhaAttemptId = previous.zerodhaAttemptId;
|
|
1545
|
-
}
|
|
1546
1851
|
if (!sessionHasCookies(next)) {
|
|
1547
1852
|
if (index >= 0) profileRecord.sessions.splice(index, 1);
|
|
1548
1853
|
} else if (index >= 0) {
|
|
@@ -1563,32 +1868,11 @@ var CliProfileStore = class {
|
|
|
1563
1868
|
if (index < 0) return;
|
|
1564
1869
|
const stored = profileRecord.sessions[index];
|
|
1565
1870
|
delete stored.operatorSessionCookie;
|
|
1566
|
-
delete stored.zerodhaAttemptId;
|
|
1567
1871
|
stored.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1568
1872
|
this.cleanEmptySession(profileRecord, index);
|
|
1569
1873
|
if (profileRecord.sessions.length === 0) delete record.profiles[profile];
|
|
1570
1874
|
});
|
|
1571
1875
|
}
|
|
1572
|
-
async getZerodhaAttempt(profile, target) {
|
|
1573
|
-
assertClientProfileName(profile);
|
|
1574
|
-
return this.withLock(async () => {
|
|
1575
|
-
const session = (await this.readRecord()).profiles[profile]?.sessions.find((value) => targetsEqual(value, target));
|
|
1576
|
-
return session?.operatorSessionCookie ? session.zerodhaAttemptId : void 0;
|
|
1577
|
-
});
|
|
1578
|
-
}
|
|
1579
|
-
async saveZerodhaAttempt(profile, target, attemptId) {
|
|
1580
|
-
assertClientProfileName(profile);
|
|
1581
|
-
if (attemptId !== void 0 && !/^[a-zA-Z0-9_-]{1,200}$/.test(attemptId)) throw new Error("Invalid Zerodha attempt identifier.");
|
|
1582
|
-
await this.update((record) => {
|
|
1583
|
-
const session = record.profiles[profile]?.sessions.find((value) => targetsEqual(value, target));
|
|
1584
|
-
if (!session?.operatorSessionCookie) {
|
|
1585
|
-
if (attemptId) throw new Error("An Operator Session is required to save a Zerodha login attempt.");
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
if (attemptId) session.zerodhaAttemptId = attemptId;
|
|
1589
|
-
else delete session.zerodhaAttemptId;
|
|
1590
|
-
});
|
|
1591
|
-
}
|
|
1592
1876
|
async removeTarget(profile, target) {
|
|
1593
1877
|
assertClientProfileName(profile);
|
|
1594
1878
|
await this.update((record) => {
|
|
@@ -1716,7 +2000,6 @@ var CliProfileStore = class {
|
|
|
1716
2000
|
operatorId: session.operatorId,
|
|
1717
2001
|
...session.operatorSessionCookie ? { operatorSessionCookie: session.operatorSessionCookie } : {},
|
|
1718
2002
|
...session.clientIdentityCookie ? { clientIdentityCookie: session.clientIdentityCookie } : {},
|
|
1719
|
-
...session.zerodhaAttemptId ? { zerodhaAttemptId: session.zerodhaAttemptId } : {},
|
|
1720
2003
|
updatedAt: session.updatedAt
|
|
1721
2004
|
}))
|
|
1722
2005
|
}
|
|
@@ -1749,46 +2032,161 @@ var CliProfileStore = class {
|
|
|
1749
2032
|
}
|
|
1750
2033
|
};
|
|
1751
2034
|
|
|
1752
|
-
// src/zerodha-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
}
|
|
1783
|
-
|
|
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));
|
|
2067
|
+
}
|
|
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);
|
|
1784
2075
|
}
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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 };
|
|
1789
2186
|
}
|
|
1790
2187
|
|
|
1791
2188
|
// src/commands.ts
|
|
2189
|
+
init_zerodha_actions();
|
|
1792
2190
|
init_gate_api();
|
|
1793
2191
|
|
|
1794
2192
|
// src/validation-scenario.ts
|
|
@@ -2053,17 +2451,19 @@ function parseTimeout(raw) {
|
|
|
2053
2451
|
return Math.ceil(value);
|
|
2054
2452
|
}
|
|
2055
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.");
|
|
2056
2455
|
if (argv.length === 0 || argv[0] === "interactive") {
|
|
2057
2456
|
throw new InvocationError("Choose a headless command or run `live-market-terminal interactive`.");
|
|
2058
2457
|
}
|
|
2059
2458
|
const options = /* @__PURE__ */ new Map();
|
|
2060
2459
|
let credentialStdin = false;
|
|
2460
|
+
let otpStdin = false;
|
|
2061
2461
|
const positional = [];
|
|
2062
2462
|
for (let index = 0; index < argv.length; index += 1) {
|
|
2063
2463
|
const token = argv[index];
|
|
2064
|
-
if (token
|
|
2065
|
-
if (
|
|
2066
|
-
|
|
2464
|
+
if (token === "--otp-stdin") {
|
|
2465
|
+
if (otpStdin) throw new InvocationError("Argument --otp-stdin may be specified only once.");
|
|
2466
|
+
otpStdin = true;
|
|
2067
2467
|
continue;
|
|
2068
2468
|
}
|
|
2069
2469
|
if (token === "--credential-stdin") {
|
|
@@ -2071,7 +2471,7 @@ function splitHeadlessArgv(argv) {
|
|
|
2071
2471
|
credentialStdin = true;
|
|
2072
2472
|
continue;
|
|
2073
2473
|
}
|
|
2074
|
-
if (token === "--env" || token === "--profile" || token === "--timeout" || token === "--attempt" || token === "--
|
|
2474
|
+
if (token === "--env" || token === "--profile" || token === "--timeout" || token === "--attempt" || token === "--date") {
|
|
2075
2475
|
const value = argv[index + 1];
|
|
2076
2476
|
if (value === void 0 || value.startsWith("--")) {
|
|
2077
2477
|
throw new InvocationError(`Invalid or incomplete command argument: ${token}`);
|
|
@@ -2086,10 +2486,32 @@ function splitHeadlessArgv(argv) {
|
|
|
2086
2486
|
}
|
|
2087
2487
|
positional.push(token);
|
|
2088
2488
|
}
|
|
2089
|
-
return { positional, options, credentialStdin };
|
|
2489
|
+
return { positional, options, credentialStdin, otpStdin };
|
|
2090
2490
|
}
|
|
2091
2491
|
function parseArguments(argv) {
|
|
2092
|
-
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.");
|
|
2093
2515
|
if (positional[0] === "zerodha") {
|
|
2094
2516
|
const action = positional[1];
|
|
2095
2517
|
if (!["login", "observe", "otp", "status"].includes(action) || positional.length !== 2 || credentialStdin) {
|
|
@@ -2103,22 +2525,41 @@ function parseArguments(argv) {
|
|
|
2103
2525
|
if (attemptId && (action === "login" || action === "status") || attemptId && !/^[a-zA-Z0-9_-]{1,200}$/.test(attemptId)) {
|
|
2104
2526
|
throw new InvocationError("--attempt must be a valid Zerodha attempt ID and is only supported for observe and otp.");
|
|
2105
2527
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2528
|
+
if ((action === "observe" || action === "otp") && !attemptId) {
|
|
2529
|
+
throw new InvocationError("--attempt ID is required for zerodha observe and otp. Use the attemptId returned by zerodha login.");
|
|
2530
|
+
}
|
|
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.");
|
|
2109
2533
|
}
|
|
2110
|
-
if (otp !== void 0 && !/^\d{6}$/.test(otp)) throw new InvocationError("OTP must be six digits.");
|
|
2111
2534
|
return {
|
|
2112
2535
|
kind: "zerodha",
|
|
2113
2536
|
action,
|
|
2114
2537
|
environment: environment2,
|
|
2115
2538
|
profile: profile2,
|
|
2116
2539
|
attemptId,
|
|
2117
|
-
|
|
2540
|
+
otpStdin,
|
|
2118
2541
|
...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
|
|
2119
2542
|
};
|
|
2120
2543
|
}
|
|
2121
|
-
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.");
|
|
2122
2563
|
if (positional[0] === "credential") {
|
|
2123
2564
|
if (positional[1] !== "provision" || positional.length > 2) {
|
|
2124
2565
|
throw new InvocationError("Use `credential provision` to provision the protected SMS credential.");
|
|
@@ -2144,9 +2585,9 @@ function parseArguments(argv) {
|
|
|
2144
2585
|
throw new InvocationError("Unexpected positional argument; use --help for command syntax.");
|
|
2145
2586
|
}
|
|
2146
2587
|
} else if (positional.length === 0) {
|
|
2147
|
-
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.");
|
|
2148
2589
|
} else {
|
|
2149
|
-
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.");
|
|
2150
2591
|
}
|
|
2151
2592
|
const environment = options.get("--env");
|
|
2152
2593
|
const profile = options.get("--profile");
|
|
@@ -2163,11 +2604,11 @@ function validateCommandProfile(profile) {
|
|
|
2163
2604
|
}
|
|
2164
2605
|
}
|
|
2165
2606
|
function durationFor(command) {
|
|
2166
|
-
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;
|
|
2167
2608
|
}
|
|
2168
2609
|
function errorDetails(error, signal) {
|
|
2169
2610
|
if (signal?.aborted) return { code: "deadline_exceeded", message: "The action exceeded its deadline.", exitCode: 1 };
|
|
2170
|
-
if (error instanceof InvocationError) {
|
|
2611
|
+
if (error instanceof InvocationError || error instanceof ProtectedInputError) {
|
|
2171
2612
|
return { code: error.code, message: error.message, exitCode: 2 };
|
|
2172
2613
|
}
|
|
2173
2614
|
if (error instanceof OperatorSessionRejectedError) {
|
|
@@ -2176,6 +2617,11 @@ function errorDetails(error, signal) {
|
|
|
2176
2617
|
if (error instanceof OperatorCredentialsRejectedError) {
|
|
2177
2618
|
return { code: "incorrect_credentials", message: "Gate rejected the Operator Login credentials.", exitCode: 1 };
|
|
2178
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
|
+
}
|
|
2179
2625
|
if (error instanceof SmsReadError) {
|
|
2180
2626
|
return { code: error.code, message: error.message, exitCode: 1 };
|
|
2181
2627
|
}
|
|
@@ -2221,18 +2667,25 @@ async function readProtectedInput(stdin, signal, label = "SMS credential") {
|
|
|
2221
2667
|
return credential;
|
|
2222
2668
|
}
|
|
2223
2669
|
async function runHeadlessCli(argv, dependencies = {}) {
|
|
2224
|
-
|
|
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) };
|
|
2225
2675
|
const started = performance.now();
|
|
2226
2676
|
let command;
|
|
2227
2677
|
let target;
|
|
2228
2678
|
let profile;
|
|
2229
2679
|
let actionApi;
|
|
2230
2680
|
let signal;
|
|
2681
|
+
let deadlineTimer;
|
|
2231
2682
|
let store;
|
|
2232
2683
|
try {
|
|
2233
2684
|
command = parseArguments(argv);
|
|
2234
2685
|
const timeoutMs = durationFor(command);
|
|
2235
|
-
|
|
2686
|
+
const deadline = new AbortController();
|
|
2687
|
+
deadlineTimer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
2688
|
+
signal = deadline.signal;
|
|
2236
2689
|
profile = command.profile;
|
|
2237
2690
|
try {
|
|
2238
2691
|
target = resolveCliTarget({ LMT_ENV: command.environment }, dependencies.configPath);
|
|
@@ -2255,6 +2708,33 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2255
2708
|
}, streams);
|
|
2256
2709
|
return 0;
|
|
2257
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
|
+
}
|
|
2258
2738
|
if (command.kind === "validate") {
|
|
2259
2739
|
const validation = await runOperatorSessionValidation({
|
|
2260
2740
|
target,
|
|
@@ -2292,42 +2772,37 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2292
2772
|
...dependencies.readLoginCredentials ? { readLoginCredentials: dependencies.readLoginCredentials } : {}
|
|
2293
2773
|
});
|
|
2294
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
|
+
}
|
|
2295
2795
|
if (command.kind === "zerodha") {
|
|
2296
2796
|
await actions.status();
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
if (command.action === "login" && otp && attemptId) {
|
|
2307
|
-
try {
|
|
2308
|
-
const pending = await runZerodhaAction(actionApi, "observe", target.operatorId, signal, attemptId);
|
|
2309
|
-
if (pending.status === "succeeded") completed = pending;
|
|
2310
|
-
} catch (error) {
|
|
2311
|
-
if (!(error instanceof ZerodhaLoginFailedError || error instanceof ZerodhaLoginAttemptUnavailableError) || signal.aborted) throw error;
|
|
2312
|
-
await store.saveZerodhaAttempt(profile, target);
|
|
2313
|
-
attemptId = void 0;
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
const action = command.action === "login" && otp && attemptId ? "otp" : command.action;
|
|
2317
|
-
const saveAttempt = (id) => store.saveZerodhaAttempt(profile, target, id);
|
|
2318
|
-
let zerodha;
|
|
2319
|
-
let startedFresh = action === "login";
|
|
2320
|
-
try {
|
|
2321
|
-
zerodha = completed ?? await runZerodhaAction(actionApi, action, target.operatorId, signal, attemptId, otp, saveAttempt);
|
|
2322
|
-
} catch (error) {
|
|
2323
|
-
if (!(error instanceof ZerodhaLoginAttemptUnavailableError) || command.action !== "login" || !otp || signal.aborted) throw error;
|
|
2324
|
-
startedFresh = true;
|
|
2325
|
-
zerodha = await runZerodhaAction(actionApi, "login", target.operatorId, signal, void 0, void 0, saveAttempt);
|
|
2326
|
-
}
|
|
2327
|
-
if (command.action === "login" && otp && zerodha.status === "awaiting_otp" && startedFresh) {
|
|
2328
|
-
zerodha = await runZerodhaAction(actionApi, "otp", target.operatorId, signal, zerodha.attemptId, otp);
|
|
2329
|
-
}
|
|
2330
|
-
if (zerodha.status === "succeeded") await store.saveZerodhaAttempt(profile, target);
|
|
2797
|
+
if (command.otpStdin) otp = await readProtectedOtp(dependencies.stdin ?? process.stdin, signal);
|
|
2798
|
+
const zerodha = await runZerodhaAction(
|
|
2799
|
+
actionApi,
|
|
2800
|
+
command.action,
|
|
2801
|
+
target.operatorId,
|
|
2802
|
+
signal,
|
|
2803
|
+
command.attemptId,
|
|
2804
|
+
otp
|
|
2805
|
+
);
|
|
2331
2806
|
const awaitingOtp = zerodha.status === "awaiting_otp";
|
|
2332
2807
|
emit({
|
|
2333
2808
|
version: 1,
|
|
@@ -2377,20 +2852,22 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2377
2852
|
}, streams);
|
|
2378
2853
|
return 0;
|
|
2379
2854
|
} catch (error) {
|
|
2380
|
-
if ((error instanceof ZerodhaLoginFailedError || error instanceof ZerodhaLoginAttemptUnavailableError) && command?.kind === "zerodha" && !command.attemptId && store && target && profile && !signal?.aborted) {
|
|
2381
|
-
await store.saveZerodhaAttempt(profile, target).catch(() => void 0);
|
|
2382
|
-
}
|
|
2383
2855
|
if (error instanceof OperatorSessionRejectedError && store && actionApi && target && profile && !signal?.aborted) {
|
|
2384
2856
|
await store.saveSession(profile, target, actionApi.getCookies()).catch(() => void 0);
|
|
2385
2857
|
}
|
|
2386
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);
|
|
2387
2861
|
emit({
|
|
2388
2862
|
version: 1,
|
|
2389
|
-
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 ?? "",
|
|
2390
2864
|
outcome: details.exitCode === 4 ? "blocked" : "fail",
|
|
2391
2865
|
reason: details.code,
|
|
2392
2866
|
...target ? { target: { environment: target.environment, origin: target.origin } } : command ? { target: { environment: command.environment } } : {},
|
|
2393
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 } : {},
|
|
2394
2871
|
elapsedMs: Math.round(performance.now() - started),
|
|
2395
2872
|
metadata: {
|
|
2396
2873
|
requestId: actionApi?.responseMetadata().requestId ?? null,
|
|
@@ -2399,8 +2876,14 @@ async function runHeadlessCli(argv, dependencies = {}) {
|
|
|
2399
2876
|
error: { code: details.code, message: details.message }
|
|
2400
2877
|
}, streams, details.exitCode === 2 ? `Invalid CLI invocation: ${details.message}` : details.message);
|
|
2401
2878
|
return details.exitCode;
|
|
2879
|
+
} finally {
|
|
2880
|
+
otp = void 0;
|
|
2881
|
+
if (deadlineTimer) clearTimeout(deadlineTimer);
|
|
2402
2882
|
}
|
|
2403
2883
|
}
|
|
2884
|
+
function hasArgumentOtp(argv) {
|
|
2885
|
+
return argv.some((token) => token === "--otp" || token.startsWith("--otp="));
|
|
2886
|
+
}
|
|
2404
2887
|
|
|
2405
2888
|
// src/index.ts
|
|
2406
2889
|
init_profile_name();
|
|
@@ -2422,23 +2905,36 @@ Usage (lmt is an alias of live-market-terminal):
|
|
|
2422
2905
|
live-market-terminal interactive credential provision [--env local|dev] [--profile NAME]
|
|
2423
2906
|
live-market-terminal login --env local|dev --profile NAME [--timeout DURATION]
|
|
2424
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]
|
|
2425
2909
|
live-market-terminal status --env local|dev --profile NAME [--timeout DURATION]
|
|
2426
|
-
live-market-terminal zerodha login --env local|dev --profile NAME [--
|
|
2427
|
-
live-market-terminal zerodha observe
|
|
2428
|
-
live-market-terminal zerodha otp
|
|
2910
|
+
live-market-terminal zerodha login --env local|dev --profile NAME [--timeout DURATION]
|
|
2911
|
+
live-market-terminal zerodha observe --attempt ID --env local|dev --profile NAME [--timeout DURATION]
|
|
2912
|
+
live-market-terminal zerodha otp --attempt ID --otp-stdin --env local|dev --profile NAME [--timeout DURATION]
|
|
2429
2913
|
live-market-terminal zerodha status --env local|dev --profile NAME [--timeout DURATION]
|
|
2430
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]
|
|
2431
2916
|
live-market-terminal credential provision --env local|dev --profile NAME --credential-stdin
|
|
2432
2917
|
|
|
2433
|
-
Headless options (--env, --profile, --timeout, --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.
|
|
2434
2919
|
|
|
2435
2920
|
From the monorepo root:
|
|
2436
2921
|
pnpm cli [--profile NAME] # interactive; --env defaults to local
|
|
2437
2922
|
pnpm cli --env dev --profile my-dev-session
|
|
2438
2923
|
pnpm -s cli:headless status --env local --profile default # build step runs quietly (-s)
|
|
2439
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
|
+
|
|
2440
2928
|
Targets: local and dev only. Production is not permitted.
|
|
2441
2929
|
|
|
2930
|
+
Zerodha login is two steps: zerodha login, then zerodha otp --attempt ID --otp-stdin.
|
|
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.
|
|
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.
|
|
2442
2938
|
Interactive mode restores a saved Operator Session when Gate still accepts it.
|
|
2443
2939
|
Headless commands write one JSON object per line to stdout; diagnostics go to stderr.
|
|
2444
2940
|
|
|
@@ -2453,8 +2949,9 @@ Options:
|
|
|
2453
2949
|
--env local|dev Target environment (interactive default: local; headless: required)
|
|
2454
2950
|
--profile NAME Client profile (default: default for interactive)
|
|
2455
2951
|
--timeout DURATION Action deadline, e.g. 45s, 2m, 30000ms (headless)
|
|
2456
|
-
--
|
|
2457
|
-
--
|
|
2952
|
+
--date YYYY-MM-DD Single IST date for candle-export; omitted: latest completed exchange session
|
|
2953
|
+
--attempt ID Attempt ID returned by zerodha login (required for observe and otp)
|
|
2954
|
+
--otp-stdin Read one six-digit OTP line from piped protected stdin (maximum 8 bytes)
|
|
2458
2955
|
--credential-stdin Read SMS credential from piped stdin (headless provision)
|
|
2459
2956
|
--help, -h Show this help
|
|
2460
2957
|
|
|
@@ -2474,6 +2971,10 @@ async function bootstrapCli(dependencies = {}) {
|
|
|
2474
2971
|
return;
|
|
2475
2972
|
}
|
|
2476
2973
|
const argv = normalizeArgv(process.argv.slice(2));
|
|
2974
|
+
if (hasArgumentOtp(argv)) {
|
|
2975
|
+
process.exitCode = await runHeadlessCli(argv);
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2477
2978
|
if (wantsCliHelp(argv)) {
|
|
2478
2979
|
process.stdout.write(formatCliHelp());
|
|
2479
2980
|
return;
|
|
@@ -2528,6 +3029,7 @@ async function bootstrapCli(dependencies = {}) {
|
|
|
2528
3029
|
};
|
|
2529
3030
|
const app = render(createElement(LoginApp2, {
|
|
2530
3031
|
api,
|
|
3032
|
+
expectedOperatorId: target.operatorId,
|
|
2531
3033
|
actionTimeoutMs: 3e4,
|
|
2532
3034
|
async onResumeSession() {
|
|
2533
3035
|
return actions.resumeInteractiveSession();
|
|
@@ -2547,6 +3049,16 @@ async function bootstrapCli(dependencies = {}) {
|
|
|
2547
3049
|
});
|
|
2548
3050
|
return result;
|
|
2549
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
|
+
}),
|
|
2550
3062
|
onLogout: () => actions.logout()
|
|
2551
3063
|
}));
|
|
2552
3064
|
await app.waitUntilExit();
|