@ai-digital-factory/lmt-cli 0.1.2 → 0.1.4

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