@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.
Files changed (3) hide show
  1. package/README.md +45 -18
  2. package/dist/index.js +711 -199
  3. 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
- if (!cookies.operatorSessionCookie) return [];
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
- throw new Error(messageFromBody(body, "Could not start Login with Zerodha.", sessionSecrets()));
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
- const { done, value } = await reader.read();
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
- throw new Error(messageFromBody(body, "Zerodha Session is not present on the Broker.", sessionSecrets()));
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
- if (event.type === "attempt_error") return;
701
- if (event.status === "awaiting_otp") {
702
- setError(event.message ?? "");
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
- setStep("otp");
705
- return;
706
- }
707
- if (event.status === "loading") {
708
- setStep("verifying");
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
- setError("Login with Zerodha exceeded its deadline.");
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
- try {
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
- try {
764
- await api.submitOtp(attemptId.current, otp);
765
- if (watcher.current?.signal.aborted) return;
766
- setOtp("");
767
- } catch (cause) {
768
- if (watcher.current?.signal.aborted) return;
769
- setError(cause instanceof Error ? cause.message : "Could not submit the Zerodha OTP.");
770
- setStep("otp");
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.return) void submitOtp();
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, ".lmt");
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-actions.ts
1753
- async function runZerodhaAction(api, action, operatorId, signal, attemptId, otp, onStarted) {
1754
- if (action === "status") {
1755
- await api.confirmZerodhaSession();
1756
- return { status: "succeeded" };
1757
- }
1758
- if (action === "login") {
1759
- const attempt = await api.startLogin();
1760
- if (attempt.operatorId !== operatorId) throw new Error("Zerodha login reached an unexpected Operator.");
1761
- attemptId = attempt.attemptId;
1762
- await onStarted?.(attemptId);
1763
- }
1764
- if (!attemptId) throw new Error("Zerodha login requires an attempt identifier.");
1765
- if (action === "otp") {
1766
- if (!otp) throw new Error("Zerodha OTP input is required.");
1767
- await api.submitOtp(attemptId, otp);
1768
- }
1769
- const observer = new AbortController();
1770
- const observationSignal = AbortSignal.any([signal, observer.signal]);
1771
- let result;
1772
- await api.watchLoginAttempt(attemptId, (event) => {
1773
- if (event.attemptId !== attemptId || event.type === "status" && event.operatorId !== operatorId) {
1774
- throw new Error("Origin returned an unexpected Zerodha login attempt.");
1775
- }
1776
- if (event.type === "status" && (event.status === "awaiting_otp" || event.status === "succeeded")) {
1777
- result = {
1778
- status: event.status,
1779
- attemptId,
1780
- ...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts },
1781
- ...event.message ? { message: event.message } : {}
1782
- };
1783
- observer.abort();
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
- }, observationSignal);
1786
- if (signal.aborted || !result) throw new Error("Zerodha login observation exceeded its deadline.");
1787
- if (result.status === "succeeded") await api.confirmZerodhaSession();
1788
- return result;
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.startsWith("--otp=")) {
2065
- if (options.has("--otp")) throw new InvocationError("Argument --otp may be specified only once.");
2066
- options.set("--otp", token.slice("--otp=".length));
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 === "--otp") {
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
- const otp = options.get("--otp");
2107
- if (action === "otp" && otp === void 0 || otp !== void 0 && action !== "otp" && action !== "login") {
2108
- throw new InvocationError("--otp VALUE is supported for zerodha login and required for zerodha otp.");
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
- otp,
2540
+ otpStdin,
2118
2541
  ...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {}
2119
2542
  };
2120
2543
  }
2121
- if (options.has("--otp") || options.has("--attempt")) throw new InvocationError("--otp and --attempt are Zerodha options only.");
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
- const streams = { stdout: dependencies.stdout ?? defaultOut, stderr: dependencies.stderr ?? defaultErr };
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
- signal = AbortSignal.timeout(timeoutMs);
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
- const otp = command.otp;
2298
- let attemptId = command.attemptId;
2299
- if (!attemptId && (command.action === "otp" || command.action === "observe" || command.action === "login" && otp)) {
2300
- attemptId = await store.getZerodhaAttempt(profile, target);
2301
- }
2302
- if (!attemptId && (command.action === "otp" || command.action === "observe")) {
2303
- throw new InvocationError("No pending Zerodha login for this profile. Run zerodha login first.");
2304
- }
2305
- let completed;
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 [--otp CODE] [--timeout DURATION]
2427
- live-market-terminal zerodha observe [--attempt ID] --env local|dev --profile NAME [--timeout DURATION]
2428
- live-market-terminal zerodha otp [--attempt ID] --otp CODE --env local|dev --profile NAME [--timeout DURATION]
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
- --attempt ID Override the remembered Zerodha attempt (optional)
2457
- --otp CODE Six-digit OTP for zerodha login or otp
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();