@ai-digital-factory/lmt-cli 0.1.6 → 1.0.0

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.
@@ -0,0 +1,1394 @@
1
+ import {
2
+ CLIENT_PROFILE_NAME_ERROR,
3
+ CONTROLLER_LOGIN_STEP,
4
+ CONTROLLER_SESSION,
5
+ CREDENTIAL_PROVISION_STEP,
6
+ CliProfileStore,
7
+ LocalConfigError,
8
+ OperatorCredentialsRejectedError,
9
+ OperatorSessionRejectedError,
10
+ PriceCrossActionError,
11
+ SettingsActionError,
12
+ SmsReadError,
13
+ ZerodhaLoginAttemptUnavailableError,
14
+ ZerodhaLoginFailedError,
15
+ assertClientProfileName,
16
+ createGateApi,
17
+ isLoginRequired,
18
+ ownerOf,
19
+ profileNameOf,
20
+ readOperatorLoginCredentials,
21
+ reportSessionStatus,
22
+ resolveCliApiOrigin,
23
+ resolveCliTarget
24
+ } from "./chunk-4N3XUBM4.js";
25
+
26
+ // src/candle-export-download.ts
27
+ import { createHash } from "node:crypto";
28
+ import { createWriteStream } from "node:fs";
29
+ import { link, mkdtemp, rm } from "node:fs/promises";
30
+ import path from "node:path";
31
+ import { Readable, Transform } from "node:stream";
32
+ import { pipeline } from "node:stream/promises";
33
+ async function downloadCandleExport(url, output, signal, fetchImpl = fetch) {
34
+ if (new URL(url).protocol !== "https:") throw new Error("Candle Export download requires an HTTPS URL.");
35
+ const destination = path.resolve(output);
36
+ const directory = await mkdtemp(path.join(path.dirname(destination), ".lmt-candle-export-"));
37
+ try {
38
+ const response = await fetchImpl(url, { signal, credentials: "omit" });
39
+ if (!response.ok || !response.body) {
40
+ await response.body?.cancel();
41
+ throw new Error(`Candle Export download failed (HTTP ${response.status}).`);
42
+ }
43
+ const hash = createHash("sha256");
44
+ let bytes = 0;
45
+ const measure = new Transform({ transform(chunk, _encoding, callback) {
46
+ bytes += chunk.length;
47
+ hash.update(chunk);
48
+ callback(null, chunk);
49
+ } });
50
+ const temporary = path.join(directory, "workbook.xlsx");
51
+ await pipeline(
52
+ Readable.fromWeb(response.body),
53
+ measure,
54
+ createWriteStream(temporary, { flags: "wx", mode: 384 }),
55
+ { signal }
56
+ );
57
+ signal.throwIfAborted();
58
+ await link(temporary, destination);
59
+ return { path: destination, bytes, sha256: hash.digest("hex") };
60
+ } finally {
61
+ await rm(directory, { recursive: true, force: true });
62
+ }
63
+ }
64
+
65
+ // src/action-deadline.ts
66
+ function withinActionDeadline(operation, signal, deadlineMessage) {
67
+ return new Promise((resolve, reject) => {
68
+ const abort = () => reject(new Error(deadlineMessage));
69
+ if (signal.aborted) {
70
+ void operation.catch(() => void 0);
71
+ abort();
72
+ return;
73
+ }
74
+ signal.addEventListener("abort", abort, { once: true });
75
+ operation.then(
76
+ (value) => {
77
+ signal.removeEventListener("abort", abort);
78
+ resolve(value);
79
+ },
80
+ (error) => {
81
+ signal.removeEventListener("abort", abort);
82
+ reject(error);
83
+ }
84
+ );
85
+ });
86
+ }
87
+
88
+ // src/operator-actions.ts
89
+ function profileTarget(target) {
90
+ return {
91
+ environment: target.environment,
92
+ origin: target.origin,
93
+ operatorId: target.operatorId
94
+ };
95
+ }
96
+ function responseMetadata(api) {
97
+ const metadata = api.responseMetadata();
98
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
99
+ }
100
+ function combineSignals(...signals) {
101
+ const present = signals.filter((signal) => signal !== void 0);
102
+ if (present.length === 1) return present[0];
103
+ return AbortSignal.any(present);
104
+ }
105
+ function isTransientGateReachabilityError(error) {
106
+ if (!(error instanceof Error)) return false;
107
+ return /Could not reach the selected Live Market Terminal Origin/.test(error.message) || /Broker is unreachable/.test(error.message) || /Gate health returned HTTP 5\d\d/.test(error.message);
108
+ }
109
+ function waitForSignal(operation, signal) {
110
+ return new Promise((resolve, reject) => {
111
+ if (signal.aborted) {
112
+ reject(new Error("The action exceeded its deadline."));
113
+ return;
114
+ }
115
+ const onAbort = () => reject(new Error("The action exceeded its deadline."));
116
+ signal.addEventListener("abort", onAbort, { once: true });
117
+ operation.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
118
+ });
119
+ }
120
+ function createOperatorSessionActions(options) {
121
+ const selectedTarget = profileTarget(options.target);
122
+ const fetchImpl = options.fetchImpl ?? fetch;
123
+ const timeoutMs = options.timeoutMs ?? 3e4;
124
+ let api;
125
+ async function loadApi() {
126
+ if (api) return api;
127
+ const cookies = await options.store.getSession(options.profile, selectedTarget);
128
+ api = createGateApi(resolveCliApiOrigin(options.target), fetchImpl, {
129
+ ...typeof options.profile === "string" ? { profile: options.profile } : {},
130
+ ...cookies ? { cookies } : {},
131
+ timeoutMs,
132
+ ...options.signal ? { signal: options.signal } : {}
133
+ });
134
+ return api;
135
+ }
136
+ async function persistCurrentCookies(client, signal) {
137
+ const write = options.store.saveSession(options.profile, selectedTarget, client.getCookies());
138
+ if (signal) await waitForSignal(write, signal);
139
+ else await write;
140
+ }
141
+ async function withAction(action, requestedSignal) {
142
+ const deadlineAt = Date.now() + timeoutMs;
143
+ const signal = combineSignals(options.signal, requestedSignal, AbortSignal.timeout(timeoutMs));
144
+ const client = await waitForSignal(loadApi(), signal);
145
+ client.setActionSignal(signal);
146
+ try {
147
+ return await waitForSignal(action(client, signal, deadlineAt), signal);
148
+ } finally {
149
+ client.setActionSignal(options.signal);
150
+ }
151
+ }
152
+ async function resumeSessionWithClient(client, signal) {
153
+ try {
154
+ const observation = await waitForSignal(
155
+ client.confirmOperatorSessionPresent(options.target.operatorId),
156
+ signal
157
+ );
158
+ await persistCurrentCookies(client, signal);
159
+ return {
160
+ observedOperator: observation.operatorId,
161
+ ...observation.clientId ? { clientId: observation.clientId } : {},
162
+ ...responseMetadata(client) ? { metadata: responseMetadata(client) } : {}
163
+ };
164
+ } catch (error) {
165
+ if (!signal.aborted && !client.getCookies().operatorSessionCookie) {
166
+ await persistCurrentCookies(client, signal).catch(() => void 0);
167
+ }
168
+ throw error;
169
+ }
170
+ }
171
+ async function statusWithClient(client, signal) {
172
+ try {
173
+ const observation = await waitForSignal(client.status(options.target.operatorId), signal);
174
+ await persistCurrentCookies(client, signal);
175
+ return {
176
+ observedOperator: observation.operatorId,
177
+ ...observation.clientId ? { clientId: observation.clientId } : {},
178
+ ...responseMetadata(client) ? { metadata: responseMetadata(client) } : {}
179
+ };
180
+ } catch (error) {
181
+ if (!signal.aborted && !client.getCookies().operatorSessionCookie) {
182
+ await persistCurrentCookies(client, signal).catch(() => void 0);
183
+ }
184
+ throw error;
185
+ }
186
+ }
187
+ async function tryRenewOperatorActivity(client, signal) {
188
+ try {
189
+ await waitForSignal(client.reportOperatorActivity(), signal);
190
+ await persistCurrentCookies(client, signal);
191
+ } catch (error) {
192
+ if (error instanceof OperatorSessionRejectedError) throw error;
193
+ }
194
+ }
195
+ async function loginWithClient(client, username, password, signal) {
196
+ try {
197
+ await waitForSignal(client.unlock(username, password, signal), signal);
198
+ await persistCurrentCookies(client, signal);
199
+ const result = await statusWithClient(client, signal);
200
+ await tryRenewOperatorActivity(client, signal);
201
+ return result;
202
+ } catch (error) {
203
+ if (!signal.aborted) await persistCurrentCookies(client, signal).catch(() => void 0);
204
+ throw error;
205
+ }
206
+ }
207
+ return {
208
+ getApi() {
209
+ return loadApi();
210
+ },
211
+ get api() {
212
+ if (!api) throw new Error("Operator client has not been initialized.");
213
+ return api;
214
+ },
215
+ loginWithPassword(username, password, requestedSignal) {
216
+ return withAction((client, signal) => loginWithClient(client, username, password, signal), requestedSignal);
217
+ },
218
+ loginFromSms() {
219
+ return withAction(async (client, signal, deadlineAt) => {
220
+ const smsCredential = await waitForSignal(options.store.getSmsCredential(), signal);
221
+ if (!smsCredential) throw new SmsReadError("sms_credential_missing", "No protected SMS credential has been provisioned.");
222
+ const credentials = await waitForSignal(
223
+ (options.readLoginCredentials ?? readOperatorLoginCredentials)({
224
+ operatorId: options.target.operatorId,
225
+ gateScope: options.target.gateScope,
226
+ smsEndpoint: options.target.smsEndpoint,
227
+ smsCredential,
228
+ timeoutMs: Math.max(1, deadlineAt - Date.now()),
229
+ signal,
230
+ ...options.secretReader ? { secretReader: options.secretReader } : {}
231
+ }),
232
+ signal
233
+ );
234
+ return loginWithClient(client, credentials.username, credentials.password, signal);
235
+ });
236
+ },
237
+ async resumeInteractiveSession() {
238
+ const saved = await options.store.getSession(options.profile, selectedTarget);
239
+ if (!saved?.operatorSessionCookie) return { step: "username" };
240
+ const maxAttempts = 8;
241
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
242
+ try {
243
+ await withAction(async (client, signal) => {
244
+ await resumeSessionWithClient(client, signal);
245
+ await tryRenewOperatorActivity(client, signal);
246
+ });
247
+ return { step: "menu" };
248
+ } catch (error) {
249
+ const retryable = isTransientGateReachabilityError(error) && attempt < maxAttempts - 1;
250
+ if (retryable) {
251
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
252
+ continue;
253
+ }
254
+ const hint = error instanceof OperatorSessionRejectedError ? error.message : error instanceof Error ? error.message : void 0;
255
+ return { step: "username", ...hint ? { hint } : {} };
256
+ }
257
+ }
258
+ return { step: "username" };
259
+ },
260
+ reportOperatorActivity(requestedSignal) {
261
+ return withAction((client, signal) => tryRenewOperatorActivity(client, signal), requestedSignal).then(() => void 0);
262
+ },
263
+ status() {
264
+ return withAction((client, signal) => statusWithClient(client, signal));
265
+ },
266
+ statusWithBrokerProof() {
267
+ return withAction((client, signal) => statusWithClient(client, signal));
268
+ },
269
+ logout() {
270
+ return withAction(async (client, signal) => {
271
+ await waitForSignal(client.logout(), signal);
272
+ await waitForSignal(options.store.removeTarget(options.profile, selectedTarget), signal);
273
+ });
274
+ }
275
+ };
276
+ }
277
+
278
+ // src/protected-input.ts
279
+ var ProtectedInputError = class extends Error {
280
+ code = "invalid_invocation";
281
+ };
282
+ async function readProtectedOtp(stdin, signal) {
283
+ if (stdin.isTTY) throw new ProtectedInputError("OTP requires piped protected stdin; interactive mode provides masked entry.");
284
+ const chunks = [];
285
+ let size = 0;
286
+ try {
287
+ const iterator = stdin[Symbol.asyncIterator]();
288
+ while (true) {
289
+ if (signal.aborted) throw new Error("The action exceeded its deadline.");
290
+ const next = await new Promise((resolve, reject) => {
291
+ const abort = () => {
292
+ stdin.destroy?.();
293
+ reject(new Error("The action exceeded its deadline."));
294
+ };
295
+ signal.addEventListener("abort", abort, { once: true });
296
+ 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));
297
+ });
298
+ if (next.done) break;
299
+ size += typeof next.value === "string" ? Buffer.byteLength(next.value) : next.value.byteLength;
300
+ if (size > 8) {
301
+ stdin.destroy?.();
302
+ throw new ProtectedInputError("OTP input is too large; provide one six-digit line.");
303
+ }
304
+ chunks.push(Buffer.from(next.value));
305
+ }
306
+ const buffer = Buffer.concat(chunks);
307
+ try {
308
+ const value = buffer.toString("utf8");
309
+ if (!/^\d{6}(?:\r?\n)?$/.test(value)) throw new ProtectedInputError("OTP input must be one six-digit line.");
310
+ return value.slice(0, 6);
311
+ } finally {
312
+ buffer.fill(0);
313
+ }
314
+ } catch (error) {
315
+ if (signal.aborted || error instanceof ProtectedInputError) throw error;
316
+ throw new ProtectedInputError("Could not read OTP from protected stdin; provide one six-digit line.");
317
+ } finally {
318
+ for (const chunk of chunks) chunk.fill(0);
319
+ }
320
+ }
321
+
322
+ // src/zerodha-actions.ts
323
+ async function runZerodhaAction(api, action, operatorId, signal, attemptId, otp, options = {}) {
324
+ const started = Date.now();
325
+ let observedState;
326
+ let observedOperator;
327
+ api.setActionSignal?.(signal);
328
+ try {
329
+ if (signal.aborted) throw new Error("Zerodha login observation exceeded its deadline.");
330
+ if (action === "status") {
331
+ const session = await withinActionDeadline(api.confirmZerodhaSession(), signal, "Zerodha login observation exceeded its deadline.");
332
+ if (session.operatorId && session.operatorId !== operatorId) throw new Error("Origin returned an unexpected Zerodha Session Operator.");
333
+ return { status: "succeeded", observedState: "present", ...session.operatorId ? { observedOperator: session.operatorId } : {}, elapsedMs: Date.now() - started };
334
+ }
335
+ if (action === "login") {
336
+ const attempt = await withinActionDeadline(api.startLogin(), signal, "Zerodha login observation exceeded its deadline.");
337
+ attemptId = attempt.attemptId;
338
+ if (attempt.operatorId !== operatorId) throw new Error("Zerodha login reached an unexpected Operator.");
339
+ }
340
+ if (!attemptId) throw new Error("Zerodha login requires an attempt identifier.");
341
+ if (action === "otp") {
342
+ if (!otp) throw new Error("Zerodha OTP input is required.");
343
+ await withinActionDeadline(api.submitOtp(attemptId, otp), signal, "Zerodha login observation exceeded its deadline.");
344
+ }
345
+ const observer = new AbortController();
346
+ const observationSignal = AbortSignal.any([signal, observer.signal]);
347
+ let result;
348
+ try {
349
+ await withinActionDeadline(api.watchLoginAttempt(attemptId, (event) => {
350
+ if (observer.signal.aborted || signal.aborted) return;
351
+ if (event.attemptId !== attemptId || event.type === "status" && event.operatorId !== operatorId) {
352
+ throw new Error("Origin returned an unexpected Zerodha login attempt.");
353
+ }
354
+ if (event.type === "status") {
355
+ observedState = event.status;
356
+ observedOperator = event.operatorId;
357
+ }
358
+ options.onObservation?.(event);
359
+ if (event.type === "status" && (event.status === "awaiting_otp" || event.status === "succeeded")) {
360
+ result = {
361
+ status: event.status,
362
+ attemptId,
363
+ observedOperator,
364
+ observedState,
365
+ ...event.failedOtpAttempts === void 0 ? {} : { failedOtpAttempts: event.failedOtpAttempts },
366
+ ...event.message ? { message: event.message } : {}
367
+ };
368
+ observer.abort();
369
+ }
370
+ }, observationSignal), observationSignal, "Zerodha login observation exceeded its deadline.");
371
+ } catch (error) {
372
+ if (!observer.signal.aborted || !result) throw error;
373
+ }
374
+ if (signal.aborted || !result) throw new Error("Zerodha login observation exceeded its deadline.");
375
+ if (result.status === "succeeded") {
376
+ const session = await withinActionDeadline(api.confirmZerodhaSession(), signal, "Zerodha login observation exceeded its deadline.");
377
+ if (session.operatorId && session.operatorId !== operatorId) throw new Error("Origin returned an unexpected Zerodha Session Operator.");
378
+ }
379
+ return { ...result, elapsedMs: Date.now() - started };
380
+ } catch (cause) {
381
+ const error = cause instanceof Error ? cause : new Error("Zerodha action failed.");
382
+ Object.assign(error, {
383
+ ...attemptId ? { attemptId, recovery: "Observe this exact attempt with the same target and profile; start explicitly only if Gate reports it inactive." } : {},
384
+ ...observedState ? { observedState } : {},
385
+ ...observedOperator ? { observedOperator } : {},
386
+ elapsedMs: Date.now() - started
387
+ });
388
+ throw error;
389
+ } finally {
390
+ api.setActionSignal?.();
391
+ }
392
+ }
393
+
394
+ // src/zerodha-validation.ts
395
+ async function runZerodhaValidation(options) {
396
+ const started = performance.now();
397
+ const caller = options.callerProfile ?? CONTROLLER_SESSION;
398
+ const controller = new AbortController();
399
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 12e4);
400
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
401
+ const observations = [];
402
+ let api;
403
+ let otp;
404
+ let result = {
405
+ scenario: "zerodha-session",
406
+ outcome: "fail",
407
+ reason: "validation_action_failed",
408
+ exitCode: 1,
409
+ target: { environment: options.target.environment, origin: options.target.origin },
410
+ ...options.callerProfile ? { profile: options.callerProfile } : {},
411
+ ...options.attemptId ? { attemptId: options.attemptId } : {},
412
+ observations,
413
+ nextStep: null
414
+ };
415
+ 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]");
416
+ 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.";
417
+ try {
418
+ 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" });
419
+ if (options.callerProfile !== void 0) try {
420
+ assertClientProfileName(options.callerProfile);
421
+ } catch {
422
+ throw new ProtectedInputError(CLIENT_PROFILE_NAME_ERROR);
423
+ }
424
+ 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.");
425
+ if (options.readOtp && !options.attemptId) throw new ProtectedInputError("OTP submission requires an explicit --attempt ID from a prior login or validation.");
426
+ const actions = createOperatorSessionActions({
427
+ target: options.target,
428
+ profile: caller,
429
+ store: options.store,
430
+ timeoutMs: options.timeoutMs ?? 12e4,
431
+ signal,
432
+ ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
433
+ });
434
+ api = await withinActionDeadline(actions.getApi(), signal, "The Zerodha Session validation exceeded its deadline.");
435
+ const operator = await actions.status();
436
+ result.observedOperator = operator.observedOperator;
437
+ if (options.readOtp) {
438
+ otp = await withinActionDeadline(options.readOtp(signal), signal, "The Zerodha Session validation exceeded its deadline.");
439
+ if (!/^\d{6}$/.test(otp)) throw new ProtectedInputError("OTP input must be one six-digit line.");
440
+ }
441
+ const action = options.attemptId ? options.readOtp ? "otp" : "observe" : "login";
442
+ const login = await runZerodhaAction(api, action, options.target.operatorId, signal, options.attemptId, otp, {
443
+ onObservation(event) {
444
+ result.attemptId = event.attemptId;
445
+ if (event.type === "status") {
446
+ result.observedState = event.status;
447
+ result.observedOperator = event.operatorId;
448
+ observations.push(event.status);
449
+ if (event.failedOtpAttempts !== void 0) result.failedOtpAttempts = event.failedOtpAttempts;
450
+ if (event.message) result.message = safeText(event.message);
451
+ }
452
+ options.onObservation?.(event);
453
+ }
454
+ });
455
+ result.attemptId = login.attemptId;
456
+ if (login.status === "awaiting_otp") {
457
+ const rejected = !!options.readOtp && (login.failedOtpAttempts ?? 0) > 0;
458
+ result = { ...result, outcome: rejected ? "fail" : "blocked", reason: rejected ? "otp_rejected" : "awaiting_otp", exitCode: rejected ? 1 : 4, nextStep: resumeStep() };
459
+ } else {
460
+ result = { ...result, outcome: "pass", reason: "zerodha_session_validated", exitCode: 0, nextStep: null };
461
+ }
462
+ } catch (cause) {
463
+ const error = cause instanceof Error ? cause : void 0;
464
+ if (error?.attemptId) result.attemptId = error.attemptId;
465
+ if (error?.observedState) result.observedState = error.observedState;
466
+ if (error?.observedOperator) result.observedOperator = error.observedOperator;
467
+ const code = error?.code;
468
+ if (signal.aborted) {
469
+ result.reason = "deadline_exceeded";
470
+ result.message = "The Zerodha Session validation exceeded its deadline.";
471
+ } else if (cause instanceof OperatorSessionRejectedError) {
472
+ result.outcome = "blocked";
473
+ result.reason = "operator_login_required";
474
+ result.exitCode = caller === CONTROLLER_SESSION ? 4 : 3;
475
+ result.message = caller === CONTROLLER_SESSION ? "The Controller Session has no valid Operator Session." : "No valid Operator Session is available; run login with this environment and profile first.";
476
+ } else if (code && ["zerodha_credentials_missing", "secret_store_unavailable", "operator_not_configured"].includes(code)) {
477
+ result.outcome = "blocked";
478
+ result.reason = code;
479
+ result.exitCode = 4;
480
+ result.message = safeText(error.message);
481
+ } else {
482
+ result.reason = cause instanceof ProtectedInputError ? "invalid_invocation" : cause instanceof ZerodhaLoginAttemptUnavailableError ? "login_attempt_inactive" : cause instanceof ZerodhaLoginFailedError ? "zerodha_login_failed" : code ?? "validation_action_failed";
483
+ result.exitCode = cause instanceof ProtectedInputError || code === "invalid_target" ? 2 : 1;
484
+ result.message = error ? safeText(error.message) : "The Zerodha Session validation failed.";
485
+ }
486
+ result.nextStep = result.reason === "operator_login_required" ? caller === CONTROLLER_SESSION ? CONTROLLER_LOGIN_STEP : "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();
487
+ if (cause instanceof OperatorSessionRejectedError && api && !signal.aborted) {
488
+ await options.store.saveSession(caller, options.target, api.getCookies()).catch(() => void 0);
489
+ }
490
+ } finally {
491
+ otp = void 0;
492
+ clearTimeout(timer);
493
+ }
494
+ const metadata = api?.responseMetadata();
495
+ return { ...result, elapsedMs: Math.round(performance.now() - started), requestId: metadata?.requestId ?? null, buildId: metadata?.buildId ?? null };
496
+ }
497
+
498
+ // src/pair-commands.ts
499
+ import { createHash as createHash2 } from "node:crypto";
500
+ import { writeFile } from "node:fs/promises";
501
+ import path2 from "node:path";
502
+ var PairInvocationError = class extends Error {
503
+ code = "invalid_invocation";
504
+ };
505
+ var PAIR_USAGE = "Use pair list, pair assign --ce ROW --pe ROW --lots N, pair unpair PAIR_ID --confirm, or pair report PAIR_ID [--publish | --output PATH].";
506
+ var VALUE_OPTIONS = /* @__PURE__ */ new Set(["--env", "--profile", "--timeout", "--ce", "--pe", "--lots", "--output"]);
507
+ var FLAG_OPTIONS = /* @__PURE__ */ new Set(["--confirm", "--publish"]);
508
+ var PAIR_ID = /^[A-Za-z0-9_-]{1,200}$/;
509
+ function isPairInvocation(argv) {
510
+ for (let index = 0; index < argv.length; index += 1) {
511
+ const token = argv[index];
512
+ if (token === "--env" || token === "--profile" || token === "--timeout") {
513
+ index += 1;
514
+ continue;
515
+ }
516
+ if (token.startsWith("--")) continue;
517
+ return token === "pair";
518
+ }
519
+ return false;
520
+ }
521
+ function parsePairArguments(argv, parseTimeout2, validateProfile) {
522
+ const options = /* @__PURE__ */ new Map();
523
+ const flags = /* @__PURE__ */ new Set();
524
+ const positional = [];
525
+ for (let index = 0; index < argv.length; index += 1) {
526
+ const token = argv[index];
527
+ if (VALUE_OPTIONS.has(token)) {
528
+ const value = argv[index + 1];
529
+ if (value === void 0 || value.startsWith("--")) throw new PairInvocationError(`Invalid or incomplete command argument: ${token}`);
530
+ if (options.has(token)) throw new PairInvocationError(`Argument ${token} may be specified only once.`);
531
+ options.set(token, value);
532
+ index += 1;
533
+ } else if (FLAG_OPTIONS.has(token)) {
534
+ if (flags.has(token)) throw new PairInvocationError(`Argument ${token} may be specified only once.`);
535
+ flags.add(token);
536
+ } else if (token.startsWith("--")) {
537
+ throw new PairInvocationError(`Unsupported command option: ${token.split("=")[0]}`);
538
+ } else positional.push(token);
539
+ }
540
+ const [, action, pairId, ...extra] = positional;
541
+ const allowed = {
542
+ list: [],
543
+ assign: ["--ce", "--pe", "--lots"],
544
+ unpair: ["--confirm"],
545
+ report: ["--publish", "--output"]
546
+ };
547
+ if (!action || !(action in allowed) || extra.length > 0 || [...options.keys(), ...flags].some((key) => !["--env", "--profile", "--timeout", ...allowed[action]].includes(key))) {
548
+ throw new PairInvocationError(PAIR_USAGE);
549
+ }
550
+ const environment = options.get("--env"), profile = options.get("--profile");
551
+ if (!environment) throw new PairInvocationError("Headless commands require --env local|dev.");
552
+ if (profile !== void 0) validateProfile(profile);
553
+ const target = { environment, ...profile !== void 0 ? { profile } : {}, ...options.has("--timeout") ? { timeoutMs: parseTimeout2(options.get("--timeout")) } : {} };
554
+ if (action === "list") {
555
+ if (pairId !== void 0) throw new PairInvocationError(PAIR_USAGE);
556
+ return { kind: "pair", action, ...target };
557
+ }
558
+ if (action === "assign") {
559
+ if (pairId !== void 0) throw new PairInvocationError(PAIR_USAGE);
560
+ const ce = options.get("--ce"), pe = options.get("--pe"), lots = options.get("--lots");
561
+ if (!ce || !pe || !lots || !/^\d{1,3}$/.test(ce) || !/^\d{1,3}$/.test(pe) || !/^-?\d{1,6}$/.test(lots)) {
562
+ throw new PairInvocationError("pair assign requires --ce ROW --pe ROW --lots N as whole numbers.");
563
+ }
564
+ return { kind: "pair", action, ceRow: Number(ce), peRow: Number(pe), pairLots: Number(lots), ...target };
565
+ }
566
+ if (!pairId || !PAIR_ID.test(pairId)) throw new PairInvocationError(`pair ${action} requires a Price Cross pair ID from pair list.`);
567
+ if (action === "unpair") {
568
+ if (!flags.has("--confirm")) {
569
+ throw new PairInvocationError("pair unpair requires --confirm. Unpair closes any paper position at its latest complete close.");
570
+ }
571
+ return { kind: "pair", action, pairId, ...target };
572
+ }
573
+ const publish = flags.has("--publish");
574
+ if (publish && options.has("--output")) throw new PairInvocationError("--output downloads the workbook; --publish returns a URL. Choose one.");
575
+ return { kind: "pair", action: "report", pairId, publish, ...options.has("--output") ? { output: options.get("--output") } : {}, ...target };
576
+ }
577
+ async function runPairCommand(command, api) {
578
+ if (command.action === "list") return { pairs: await api.listPriceCrossPairs() };
579
+ if (command.action === "assign") {
580
+ const { pair } = await api.assignPriceCrossPair({ ceRow: command.ceRow, peRow: command.peRow, pairLots: command.pairLots });
581
+ return { reason: "pair_assigned", pair };
582
+ }
583
+ if (command.action === "unpair") {
584
+ const { pairs: _pairs, ...unpair } = await api.unpairPriceCrossPair({ pairId: command.pairId, confirmExit: true });
585
+ return { reason: unpair.outcome, unpair: { pairId: command.pairId, ...unpair } };
586
+ }
587
+ if (command.publish) {
588
+ const published = await api.publishPriceCrossReport(command.pairId);
589
+ return { reason: "report_published", report: { pairId: command.pairId, published: true, url: published.url, filename: published.filename } };
590
+ }
591
+ const download = await api.downloadPriceCrossReport(command.pairId);
592
+ const output = path2.resolve(command.output ?? path2.basename(download.filename));
593
+ await writeFile(output, download.bytes, { flag: command.output ? "w" : "wx" });
594
+ return { reason: "report_downloaded", report: {
595
+ pairId: command.pairId,
596
+ published: false,
597
+ filename: download.filename,
598
+ path: output,
599
+ bytes: download.bytes.length,
600
+ sha256: createHash2("sha256").update(download.bytes).digest("hex"),
601
+ decisionCount: download.decisionCount
602
+ } };
603
+ }
604
+
605
+ // src/validation-scenario.ts
606
+ import { randomUUID } from "node:crypto";
607
+ function makeTemporaryProfile(callerProfile = "cli") {
608
+ const suffix = `${Date.now().toString(36)}-${process.pid.toString(36)}-${randomUUID().slice(0, 8)}`;
609
+ const prefix = callerProfile.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 20) || "cli";
610
+ return `${prefix}-validate-${suffix}`;
611
+ }
612
+ function safeFailureMessage(error) {
613
+ if (error instanceof Error) {
614
+ return error.message.replace(/lmt_(?:operator_session|client_id)=[A-Za-z0-9._~-]+/g, "[REDACTED]").replace(/(password|token|credential|cookie)\s*[=:]\s*[^\s,;]+/gi, "$1=[REDACTED]");
615
+ }
616
+ return "The Operator Session validation failed.";
617
+ }
618
+ function gateUnavailable(error) {
619
+ return error instanceof Error && /Could not reach the selected Live Market Terminal Origin/.test(error.message);
620
+ }
621
+ function deadline(ms) {
622
+ const controller = new AbortController();
623
+ const timer = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), ms);
624
+ return { signal: controller.signal, clear: () => clearTimeout(timer) };
625
+ }
626
+ function waitForSignal2(operation, signal, deadlineMessage) {
627
+ return new Promise((resolve, reject) => {
628
+ if (signal.aborted) {
629
+ reject(new Error(deadlineMessage));
630
+ return;
631
+ }
632
+ const onAbort = () => reject(new Error(deadlineMessage));
633
+ signal.addEventListener("abort", onAbort, { once: true });
634
+ operation.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
635
+ });
636
+ }
637
+ function actionOptions(options, profile, timeoutMs, signal) {
638
+ return {
639
+ target: options.target,
640
+ profile,
641
+ store: options.store,
642
+ timeoutMs,
643
+ ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {},
644
+ signal,
645
+ ...options.secretReader ? { secretReader: options.secretReader } : {},
646
+ ...options.readLoginCredentials ? { readLoginCredentials: options.readLoginCredentials } : {}
647
+ };
648
+ }
649
+ async function runOperatorSessionValidation(options) {
650
+ const started = performance.now();
651
+ const actionTimeoutMs = options.timeoutMs ?? 12e4;
652
+ const actionDeadlineAt = Date.now() + actionTimeoutMs;
653
+ const actionDeadline = deadline(actionTimeoutMs);
654
+ const actionTimeoutSignal = actionDeadline.signal;
655
+ const actionSignal = options.signal ? AbortSignal.any([options.signal, actionTimeoutSignal]) : actionTimeoutSignal;
656
+ const temporaryProfile = makeTemporaryProfile(options.callerProfile);
657
+ const invalidProfile = `${temporaryProfile.slice(0, 54)}-invalid`;
658
+ const observations = [];
659
+ let outcome = "fail";
660
+ let reason = "scenario_failed";
661
+ let observedOperator;
662
+ let requestId;
663
+ let buildId;
664
+ let cleanup = { outcome: "success", removedProfiles: [] };
665
+ try {
666
+ const smsCredential = await waitForSignal2(
667
+ options.store.getSmsCredential(),
668
+ actionSignal,
669
+ "The validation exceeded its deadline."
670
+ );
671
+ if (!smsCredential) {
672
+ outcome = "blocked";
673
+ reason = "sms_credential_missing";
674
+ observations.push("Protected SMS credential is not provisioned.");
675
+ } else {
676
+ let credentials;
677
+ try {
678
+ credentials = await waitForSignal2(
679
+ (options.readLoginCredentials ?? readOperatorLoginCredentials)({
680
+ operatorId: options.target.operatorId,
681
+ gateScope: options.target.gateScope,
682
+ smsEndpoint: options.target.smsEndpoint,
683
+ smsCredential,
684
+ timeoutMs: Math.max(1, actionDeadlineAt - Date.now()),
685
+ signal: actionSignal,
686
+ ...options.secretReader ? { secretReader: options.secretReader } : {}
687
+ }),
688
+ actionSignal,
689
+ "The validation exceeded its deadline."
690
+ );
691
+ } catch (error) {
692
+ if (actionSignal.aborted) {
693
+ outcome = "fail";
694
+ reason = "deadline_exceeded";
695
+ observations.push("The validation exceeded its deadline while reading the Operator Directory.");
696
+ } else if (error instanceof SmsReadError && ["sms_credential_missing", "sms_unavailable"].includes(error.code)) {
697
+ outcome = "blocked";
698
+ reason = error.code;
699
+ observations.push("Operator Directory credentials could not be read from the selected Gate SMS scope.");
700
+ } else {
701
+ outcome = "fail";
702
+ reason = error instanceof SmsReadError ? error.code : "operator_directory_invalid";
703
+ observations.push("The selected Gate Operator Directory could not provide the configured Operator credentials.");
704
+ }
705
+ }
706
+ if (outcome === "fail" && reason === "scenario_failed" && credentials) {
707
+ const validActions = createOperatorSessionActions(actionOptions(options, temporaryProfile, Math.max(1, actionDeadlineAt - Date.now()), actionSignal));
708
+ try {
709
+ const login = await waitForSignal2(
710
+ validActions.loginWithPassword(credentials.username, credentials.password),
711
+ actionSignal,
712
+ "The validation exceeded its deadline."
713
+ );
714
+ observedOperator = login.observedOperator;
715
+ requestId = login.metadata?.requestId;
716
+ buildId = login.metadata?.buildId;
717
+ observations.push("Gate accepted the configured Operator Login credentials.");
718
+ const laterInvocation = createOperatorSessionActions(actionOptions(options, temporaryProfile, Math.max(1, actionDeadlineAt - Date.now()), actionSignal));
719
+ const reused = await waitForSignal2(laterInvocation.statusWithBrokerProof(), actionSignal, "The validation exceeded its deadline.");
720
+ observedOperator = reused.observedOperator;
721
+ requestId = reused.metadata?.requestId ?? requestId;
722
+ buildId = reused.metadata?.buildId ?? buildId;
723
+ observations.push("A fresh client invocation reused the same Gate session and observed Operator.");
724
+ const invalidActions = createOperatorSessionActions(actionOptions(options, invalidProfile, Math.max(1, actionDeadlineAt - Date.now()), actionSignal));
725
+ let invalidLoginRejected = false;
726
+ try {
727
+ await waitForSignal2(
728
+ invalidActions.loginWithPassword(credentials.username, `${credentials.password}.invalid`),
729
+ actionSignal,
730
+ "The validation exceeded its deadline."
731
+ );
732
+ } catch (error) {
733
+ if (error instanceof OperatorCredentialsRejectedError) {
734
+ invalidLoginRejected = true;
735
+ observations.push("Gate rejected invalid credentials from a client without a usable session.");
736
+ } else {
737
+ throw error;
738
+ }
739
+ }
740
+ if (!invalidLoginRejected) {
741
+ outcome = "fail";
742
+ reason = "invalid_credentials_accepted";
743
+ observations.push("Gate accepted the invalid Operator Login credentials.");
744
+ } else {
745
+ const invalidLaterInvocation = createOperatorSessionActions(actionOptions(options, invalidProfile, Math.max(1, actionDeadlineAt - Date.now()), actionSignal));
746
+ try {
747
+ await waitForSignal2(invalidLaterInvocation.statusWithBrokerProof(), actionSignal, "The validation exceeded its deadline.");
748
+ outcome = "fail";
749
+ reason = "invalid_login_established_session";
750
+ observations.push("The unauthenticated test client unexpectedly had a usable Operator Session.");
751
+ } catch (error) {
752
+ if (!(error instanceof OperatorSessionRejectedError)) throw error;
753
+ outcome = "pass";
754
+ reason = "operator_session_validated";
755
+ observations.push("Gate confirmed that the invalid-login client has no usable Operator Session.");
756
+ }
757
+ }
758
+ } catch (error) {
759
+ if (actionSignal.aborted) {
760
+ outcome = "fail";
761
+ reason = "deadline_exceeded";
762
+ observations.push("The Operator Session validation exceeded its deadline.");
763
+ } else if (gateUnavailable(error)) {
764
+ outcome = "blocked";
765
+ reason = "gate_unreachable";
766
+ observations.push("The selected Gate could not be reached during the validation scenario.");
767
+ } else {
768
+ outcome = "fail";
769
+ reason = error instanceof OperatorSessionRejectedError ? "session_rejected_after_login" : "validation_action_failed";
770
+ observations.push(safeFailureMessage(error));
771
+ }
772
+ }
773
+ }
774
+ }
775
+ } catch (error) {
776
+ if (actionSignal.aborted) {
777
+ outcome = "fail";
778
+ reason = "deadline_exceeded";
779
+ observations.push("The Operator Session validation exceeded its deadline.");
780
+ } else if (gateUnavailable(error)) {
781
+ outcome = "blocked";
782
+ reason = "gate_unreachable";
783
+ observations.push("The selected Gate could not be reached.");
784
+ } else {
785
+ outcome = "fail";
786
+ reason = "validation_action_failed";
787
+ observations.push(safeFailureMessage(error));
788
+ }
789
+ } finally {
790
+ actionDeadline.clear();
791
+ const cleanupTimeoutMs = options.cleanupTimeoutMs ?? 1e4;
792
+ const cleanupDeadline = deadline(cleanupTimeoutMs);
793
+ const cleanupSignal = cleanupDeadline.signal;
794
+ const removedProfiles = [];
795
+ let cleanupError;
796
+ for (const profile of [invalidProfile, temporaryProfile]) {
797
+ try {
798
+ const saved = await waitForSignal2(
799
+ options.store.getSession(profile, options.target),
800
+ cleanupSignal,
801
+ "Validation cleanup exceeded its deadline."
802
+ );
803
+ if (saved?.operatorSessionCookie) {
804
+ const cleanupActions = createOperatorSessionActions(actionOptions(options, profile, cleanupTimeoutMs, cleanupSignal));
805
+ try {
806
+ await waitForSignal2(cleanupActions.logout(), cleanupSignal, "Validation cleanup exceeded its deadline.");
807
+ } catch (error) {
808
+ cleanupError ??= { code: cleanupSignal.aborted ? "cleanup_deadline_exceeded" : "gate_logout_failed", message: safeFailureMessage(error) };
809
+ }
810
+ }
811
+ } catch (error) {
812
+ cleanupError ??= {
813
+ code: cleanupSignal.aborted ? "cleanup_deadline_exceeded" : "cleanup_store_read_failed",
814
+ message: safeFailureMessage(error)
815
+ };
816
+ }
817
+ try {
818
+ await waitForSignal2(
819
+ options.store.removeTarget(profile, options.target),
820
+ cleanupSignal,
821
+ "Validation cleanup exceeded its deadline."
822
+ );
823
+ removedProfiles.push(profile);
824
+ } catch (error) {
825
+ cleanupError ??= {
826
+ code: cleanupSignal.aborted ? "cleanup_deadline_exceeded" : "cleanup_store_remove_failed",
827
+ message: safeFailureMessage(error)
828
+ };
829
+ }
830
+ }
831
+ cleanupDeadline.clear();
832
+ cleanup = cleanupError ? { outcome: "fail", removedProfiles, error: cleanupError } : { outcome: "success", removedProfiles };
833
+ if (cleanupError && outcome === "pass") {
834
+ outcome = "fail";
835
+ reason = "cleanup_failed";
836
+ }
837
+ }
838
+ return {
839
+ target: { environment: options.target.environment, origin: options.target.origin },
840
+ ...options.callerProfile ? { profile: options.callerProfile } : {},
841
+ outcome,
842
+ reason,
843
+ expectedOperator: options.target.operatorId,
844
+ ...observedOperator ? { observedOperator } : {},
845
+ temporaryProfile,
846
+ observations,
847
+ elapsedMs: Math.round(performance.now() - started),
848
+ ...requestId ? { requestId } : {},
849
+ ...buildId ? { buildId } : {},
850
+ cleanup
851
+ };
852
+ }
853
+
854
+ // src/commands.ts
855
+ var defaultOut = { write: (chunk) => process.stdout.write(chunk) };
856
+ var defaultErr = { write: (chunk) => process.stderr.write(chunk) };
857
+ var ENV_REQUIRED = "Headless commands require --env local|dev.";
858
+ var InvocationError = class extends Error {
859
+ constructor(message, code = "invalid_invocation") {
860
+ super(message);
861
+ this.code = code;
862
+ }
863
+ code;
864
+ };
865
+ function parseTimeout(raw) {
866
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(raw.trim());
867
+ if (!match) throw new InvocationError("Timeout must be a finite duration such as 30000ms, 30s, or 2m.");
868
+ const value = Number(match[1]) * (match[2] === "m" ? 6e4 : match[2] === "s" ? 1e3 : 1);
869
+ if (!Number.isFinite(value) || value < 1 || value > 6e5) {
870
+ throw new InvocationError("Timeout must be between 1ms and 10m.");
871
+ }
872
+ return Math.ceil(value);
873
+ }
874
+ function splitHeadlessArgv(argv) {
875
+ if (hasArgumentOtp(argv)) throw new InvocationError("--otp arguments are no longer accepted. Use --otp-stdin with an explicit --attempt ID.");
876
+ if (argv.length === 0 || argv[0] === "interactive") {
877
+ throw new InvocationError("Choose a headless command or run `live-market-terminal interactive`.");
878
+ }
879
+ const options = /* @__PURE__ */ new Map();
880
+ let credentialStdin = false;
881
+ let otpStdin = false;
882
+ const positional = [];
883
+ for (let index = 0; index < argv.length; index += 1) {
884
+ const token = argv[index];
885
+ if (token === "--otp-stdin") {
886
+ if (otpStdin) throw new InvocationError("Argument --otp-stdin may be specified only once.");
887
+ otpStdin = true;
888
+ continue;
889
+ }
890
+ if (token === "--credential-stdin") {
891
+ if (credentialStdin) throw new InvocationError("Argument --credential-stdin may be specified only once.");
892
+ credentialStdin = true;
893
+ continue;
894
+ }
895
+ if (token === "--env" || token === "--profile" || token === "--timeout" || token === "--attempt" || token === "--date" || token === "--output") {
896
+ const value = argv[index + 1];
897
+ if (value === void 0 || value.startsWith("--")) {
898
+ throw new InvocationError(`Invalid or incomplete command argument: ${token}`);
899
+ }
900
+ if (options.has(token)) throw new InvocationError(`Argument ${token} may be specified only once.`);
901
+ options.set(token, value);
902
+ index += 1;
903
+ continue;
904
+ }
905
+ if (token.startsWith("--")) {
906
+ throw new InvocationError(`Unsupported command option: ${token.split("=")[0]}`);
907
+ }
908
+ positional.push(token);
909
+ }
910
+ return { positional, options, credentialStdin, otpStdin };
911
+ }
912
+ function parseArguments(argv) {
913
+ if (!hasArgumentOtp(argv) && isPairInvocation(argv)) return parsePairArguments(argv, parseTimeout, validateCommandProfile);
914
+ const parsedInput = splitHeadlessArgv(argv.filter((value) => value !== "--stdin"));
915
+ const commandVerb = parsedInput.positional[0];
916
+ if (commandVerb === "settings" || commandVerb === "token") {
917
+ const stdin = argv.includes("--stdin");
918
+ if (argv.filter((value) => value === "--stdin").length > 1) throw new InvocationError("--stdin may be supplied only once.");
919
+ const parsed = parsedInput;
920
+ const [kind2, action] = parsed.positional;
921
+ 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))) {
922
+ throw new InvocationError("Use settings read, settings save --stdin, or token replace --stdin.");
923
+ }
924
+ return { kind: kind2, action, ...commandTarget(parsed.options) };
925
+ }
926
+ if (argv.includes("--stdin")) throw new InvocationError("--stdin is only supported for Settings save and token replacement.");
927
+ const { positional, options, credentialStdin, otpStdin } = parsedInput;
928
+ if (positional[0] === "candle-export") {
929
+ const instrument = positional[1] === void 0 ? void 0 : parseCandleExportInstrument(positional[1]);
930
+ const date = options.get("--date");
931
+ if (positional.length !== 2 || !instrument || credentialStdin || otpStdin || options.has("--attempt") || options.get("--output") === "" || date !== void 0 && !isCandleExportDate(date)) {
932
+ throw new InvocationError("Use candle-export EXCHANGE:SYMBOL [--date YYYY-MM-DD] [--output PATH].");
933
+ }
934
+ const target = commandTarget(options);
935
+ return {
936
+ kind: "candle-export",
937
+ ...instrument,
938
+ ...target,
939
+ ...date ? { date } : {},
940
+ ...options.has("--output") ? { output: options.get("--output") } : {}
941
+ };
942
+ }
943
+ if (options.has("--output")) throw new InvocationError("--output is only supported for candle-export and pair report.");
944
+ if (options.has("--date")) throw new InvocationError("--date is only supported for candle-export.");
945
+ if (positional[0] === "zerodha") {
946
+ const action = positional[1];
947
+ if (!["login", "observe", "otp", "status"].includes(action) || positional.length !== 2 || credentialStdin) {
948
+ throw new InvocationError("Use zerodha login, observe, otp, or status.");
949
+ }
950
+ const target = commandTarget(options);
951
+ const attemptId = options.get("--attempt");
952
+ if (attemptId && (action === "login" || action === "status") || attemptId && !/^[a-zA-Z0-9_-]{1,200}$/.test(attemptId)) {
953
+ throw new InvocationError("--attempt must be a valid Zerodha attempt ID and is only supported for observe and otp.");
954
+ }
955
+ if ((action === "observe" || action === "otp") && !attemptId) {
956
+ throw new InvocationError("--attempt ID is required for zerodha observe and otp. Use the attemptId returned by zerodha login.");
957
+ }
958
+ if (action === "otp" && !otpStdin || otpStdin && action !== "otp") {
959
+ throw new InvocationError("--otp-stdin is required for zerodha otp only. Start with zerodha login, then submit with zerodha otp --attempt ID --otp-stdin.");
960
+ }
961
+ return {
962
+ kind: "zerodha",
963
+ action,
964
+ ...target,
965
+ attemptId,
966
+ otpStdin
967
+ };
968
+ }
969
+ if (positional[0] === "validate" && positional[1] === "zerodha-session") {
970
+ if (positional.length !== 2 || credentialStdin) throw new InvocationError("Use validate zerodha-session with optional --attempt ID and --otp-stdin.");
971
+ const target = commandTarget(options);
972
+ const attemptId = options.get("--attempt");
973
+ if (attemptId !== void 0 && !/^[a-zA-Z0-9_-]{1,200}$/.test(attemptId)) throw new InvocationError("--attempt must be a valid Zerodha attempt ID.");
974
+ if (otpStdin && !attemptId) throw new InvocationError("--otp-stdin requires an explicit --attempt ID from a prior login or validation.");
975
+ return { kind: "validate-zerodha", ...target, attemptId, otpStdin };
976
+ }
977
+ if (otpStdin || options.has("--attempt")) throw new InvocationError("--otp-stdin and --attempt are Zerodha options only.");
978
+ if (positional[0] === "credential") {
979
+ if (positional[1] !== "provision" || positional.length > 2) {
980
+ throw new InvocationError("Use `credential provision` to provision the protected SMS credential.");
981
+ }
982
+ if (!credentialStdin) throw new InvocationError("Pipe the credential with --credential-stdin, or run in a terminal to type it.");
983
+ const environment = options.get("--env");
984
+ if (!environment) throw new InvocationError("Headless credential provision requires --env local|dev and --credential-stdin.");
985
+ return { kind: "credential-provision", environment };
986
+ }
987
+ if (credentialStdin) throw new InvocationError("--credential-stdin is only supported for credential provision.");
988
+ let kind;
989
+ if (positional[0] === "login" || positional[0] === "status" || positional[0] === "logout") {
990
+ kind = positional[0];
991
+ if (positional.length > 1) {
992
+ throw new InvocationError("Unexpected positional argument; use --help for command syntax.");
993
+ }
994
+ } else if (positional[0] === "validate" && positional[1] === "operator-session") {
995
+ kind = "validate";
996
+ if (positional.length > 2) {
997
+ throw new InvocationError("Unexpected positional argument; use --help for command syntax.");
998
+ }
999
+ } else if (positional.length === 0) {
1000
+ throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session|zerodha-session.");
1001
+ } else {
1002
+ throw new InvocationError("Unknown command. Choose login, logout, status, zerodha, credential provision, or validate operator-session|zerodha-session.");
1003
+ }
1004
+ return { kind, ...commandTarget(options) };
1005
+ }
1006
+ function commandTarget(options) {
1007
+ const environment = options.get("--env");
1008
+ const profile = options.get("--profile");
1009
+ if (!environment) throw new InvocationError(ENV_REQUIRED);
1010
+ if (profile !== void 0) validateCommandProfile(profile);
1011
+ return { environment, profile, ...options.has("--timeout") ? { timeoutMs: parseTimeout(options.get("--timeout")) } : {} };
1012
+ }
1013
+ function parseCandleExportInstrument(value) {
1014
+ const instrument = /^([A-Za-z][A-Za-z0-9]{0,31}):([^:\s]{1,100})$/.exec(value);
1015
+ return instrument ? { exchange: instrument[1].toUpperCase(), tradingSymbol: instrument[2].toUpperCase() } : void 0;
1016
+ }
1017
+ function isCandleExportDate(date) {
1018
+ const parsed = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
1019
+ return /^\d{4}-\d{2}-\d{2}$/.test(date) && Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === date && date >= "2000-01-01";
1020
+ }
1021
+ function validateCommandProfile(profile) {
1022
+ try {
1023
+ assertClientProfileName(profile);
1024
+ } catch {
1025
+ throw new InvocationError(CLIENT_PROFILE_NAME_ERROR);
1026
+ }
1027
+ }
1028
+ function durationFor(command) {
1029
+ return command.kind === "validate" || command.kind === "validate-zerodha" ? command.timeoutMs ?? 12e4 : command.kind === "credential-provision" ? 3e4 : command.timeoutMs ?? 3e4;
1030
+ }
1031
+ function errorDetails(error, signal) {
1032
+ if (signal?.aborted) return { code: "deadline_exceeded", message: "The action exceeded its deadline.", exitCode: 1 };
1033
+ if (error instanceof InvocationError || error instanceof ProtectedInputError || error instanceof PairInvocationError) {
1034
+ return { code: error.code, message: error.message, exitCode: 2 };
1035
+ }
1036
+ if (error instanceof OperatorSessionRejectedError) {
1037
+ return { code: "operator_login_required", message: "No valid Operator Session is available; run login first.", exitCode: 3 };
1038
+ }
1039
+ if (error instanceof OperatorCredentialsRejectedError) {
1040
+ return { code: "incorrect_credentials", message: "Gate rejected the Operator Login credentials.", exitCode: 1 };
1041
+ }
1042
+ if (error instanceof ZerodhaLoginAttemptUnavailableError) return { code: "login_attempt_inactive", message: error.message, exitCode: 1 };
1043
+ if (error instanceof ZerodhaLoginFailedError) return { code: "zerodha_login_failed", message: error.message, exitCode: 1 };
1044
+ 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)) {
1045
+ return { code: error.code, message: error.message, exitCode: 1 };
1046
+ }
1047
+ if (error instanceof SmsReadError) {
1048
+ return { code: error.code, message: error.message, exitCode: 1 };
1049
+ }
1050
+ if (error instanceof Error && error.message === "The CLI supports local or dev targets only; prod is not permitted.") {
1051
+ return { code: "invalid_target", message: error.message, exitCode: 2 };
1052
+ }
1053
+ return {
1054
+ code: "action_failed",
1055
+ message: error instanceof Error ? error.message : "The CLI action failed.",
1056
+ exitCode: 1
1057
+ };
1058
+ }
1059
+ function controllerSessionDetails(details) {
1060
+ if (details.code === "sms_credential_missing") return { ...details, exitCode: 4, nextStep: CREDENTIAL_PROVISION_STEP };
1061
+ if (isLoginRequired(details.code)) {
1062
+ return { ...details, exitCode: 4, nextStep: CONTROLLER_LOGIN_STEP };
1063
+ }
1064
+ return details;
1065
+ }
1066
+ function emitLine(line, streams, diagnostic) {
1067
+ streams.stdout.write(`${JSON.stringify(line)}
1068
+ `);
1069
+ if (diagnostic) streams.stderr.write(`${diagnostic}
1070
+ `);
1071
+ }
1072
+ function emit(envelope, streams, diagnostic) {
1073
+ emitLine(envelope, streams, diagnostic);
1074
+ }
1075
+ async function readProtectedInput(stdin, signal, label = "SMS credential") {
1076
+ if (!stdin || stdin.isTTY) throw new InvocationError(`${label} requires piped protected stdin.`);
1077
+ const chunks = [];
1078
+ let size = 0;
1079
+ const iterator = stdin[Symbol.asyncIterator]();
1080
+ while (true) {
1081
+ if (signal.aborted) throw new Error("The action exceeded its deadline.");
1082
+ const next = await new Promise((resolve, reject) => {
1083
+ const onAbort = () => {
1084
+ stdin.destroy?.();
1085
+ reject(new Error("The action exceeded its deadline."));
1086
+ };
1087
+ signal.addEventListener("abort", onAbort, { once: true });
1088
+ iterator.next().then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
1089
+ });
1090
+ if (next.done) break;
1091
+ const chunk = next.value;
1092
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1093
+ size += buffer.length;
1094
+ if (size > 4096) throw new InvocationError(`${label} input is too large.`);
1095
+ chunks.push(buffer);
1096
+ }
1097
+ const credential = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
1098
+ if (!credential.trim() || /[\r\n]/.test(credential)) throw new InvocationError(`${label} input must be one non-empty line.`);
1099
+ return credential;
1100
+ }
1101
+ async function runHeadlessCli(argv, dependencies = {}) {
1102
+ let otp;
1103
+ let securePayload;
1104
+ const protect = (stream) => ({ write(chunk) {
1105
+ let output = otp ? chunk.replaceAll(otp, "[redacted]") : chunk;
1106
+ if (securePayload) output = output.replaceAll(securePayload, "[redacted]");
1107
+ return stream.write(output);
1108
+ } });
1109
+ const streams = { stdout: protect(dependencies.stdout ?? defaultOut), stderr: protect(dependencies.stderr ?? defaultErr) };
1110
+ const started = performance.now();
1111
+ let command;
1112
+ let target;
1113
+ let profile;
1114
+ let owner;
1115
+ let actionApi;
1116
+ let signal;
1117
+ let deadlineTimer;
1118
+ let store;
1119
+ try {
1120
+ command = parseArguments(argv);
1121
+ const timeoutMs = durationFor(command);
1122
+ const deadline2 = new AbortController();
1123
+ deadlineTimer = setTimeout(() => deadline2.abort(), timeoutMs);
1124
+ signal = deadline2.signal;
1125
+ profile = command.profile;
1126
+ try {
1127
+ target = resolveCliTarget({ LMT_ENV: command.environment }, dependencies.configPath, dependencies.workingDirectory);
1128
+ } catch (error) {
1129
+ if (error instanceof LocalConfigError) throw new InvocationError(error.message, error.code);
1130
+ throw new InvocationError(error instanceof Error ? error.message : "The selected target is invalid.", "invalid_target");
1131
+ }
1132
+ store = new CliProfileStore(dependencies.homeDirectory);
1133
+ if (command.kind === "credential-provision") {
1134
+ const credential = await readProtectedInput(dependencies.stdin ?? process.stdin, signal);
1135
+ await store.saveSmsCredential(credential);
1136
+ streams.stdout.write(`${JSON.stringify({ version: 1, command: "credential provision", outcome: "success", environment: target.environment })}
1137
+ `);
1138
+ return 0;
1139
+ }
1140
+ profile = command.profile;
1141
+ owner = ownerOf(profile);
1142
+ if (command.kind === "status") {
1143
+ const { report, exitCode } = await reportSessionStatus({
1144
+ target,
1145
+ owner,
1146
+ store,
1147
+ timeoutMs,
1148
+ signal,
1149
+ ...dependencies.fetchImpl ? { fetchImpl: dependencies.fetchImpl } : {}
1150
+ });
1151
+ emitLine(report, streams, report.outcome === "success" ? void 0 : report.error?.message ?? report.nextStep);
1152
+ return exitCode;
1153
+ }
1154
+ if (command.kind === "validate-zerodha") {
1155
+ const validation = await runZerodhaValidation({
1156
+ target,
1157
+ callerProfile: profile,
1158
+ store,
1159
+ timeoutMs,
1160
+ signal,
1161
+ attemptId: command.attemptId,
1162
+ ...command.otpStdin ? { readOtp: (inputSignal) => readProtectedOtp(dependencies.stdin ?? process.stdin, inputSignal) } : {},
1163
+ ...dependencies.fetchImpl ? { fetchImpl: dependencies.fetchImpl } : {}
1164
+ });
1165
+ emit({
1166
+ version: 1,
1167
+ command: "validate zerodha-session",
1168
+ outcome: validation.outcome,
1169
+ reason: validation.reason,
1170
+ target: validation.target,
1171
+ profile,
1172
+ observedOperator: validation.observedOperator,
1173
+ attemptId: validation.attemptId,
1174
+ observedState: validation.observedState,
1175
+ elapsedMs: validation.elapsedMs,
1176
+ metadata: { requestId: validation.requestId, buildId: validation.buildId },
1177
+ validation
1178
+ }, streams, validation.exitCode === 0 ? void 0 : `Zerodha Session validation ${validation.outcome}: ${validation.reason}. ${validation.nextStep ?? ""}`);
1179
+ return validation.exitCode;
1180
+ }
1181
+ if (command.kind === "validate") {
1182
+ const validation = await runOperatorSessionValidation({
1183
+ target,
1184
+ callerProfile: profile,
1185
+ store,
1186
+ timeoutMs,
1187
+ signal,
1188
+ ...dependencies.fetchImpl ? { fetchImpl: dependencies.fetchImpl } : {},
1189
+ ...dependencies.secretReader ? { secretReader: dependencies.secretReader } : {},
1190
+ ...dependencies.readLoginCredentials ? { readLoginCredentials: dependencies.readLoginCredentials } : {}
1191
+ });
1192
+ const exitCode = validation.outcome === "pass" ? 0 : validation.outcome === "blocked" ? 4 : 1;
1193
+ emit({
1194
+ version: 1,
1195
+ command: "validate operator-session",
1196
+ outcome: validation.outcome,
1197
+ reason: validation.reason,
1198
+ target: { environment: target.environment, origin: target.origin },
1199
+ profile,
1200
+ ...validation.observedOperator ? { observedOperator: validation.observedOperator } : {},
1201
+ elapsedMs: validation.elapsedMs,
1202
+ metadata: { requestId: validation.requestId ?? null, buildId: validation.buildId ?? null },
1203
+ validation
1204
+ }, streams, exitCode === 0 ? void 0 : `Operator Session validation ${validation.outcome}: ${validation.reason}.`);
1205
+ return exitCode;
1206
+ }
1207
+ const actions = createOperatorSessionActions({
1208
+ target,
1209
+ profile: owner,
1210
+ store,
1211
+ timeoutMs,
1212
+ signal,
1213
+ ...dependencies.fetchImpl ? { fetchImpl: dependencies.fetchImpl } : {},
1214
+ ...dependencies.secretReader ? { secretReader: dependencies.secretReader } : {},
1215
+ ...dependencies.readLoginCredentials ? { readLoginCredentials: dependencies.readLoginCredentials } : {}
1216
+ });
1217
+ actionApi = await actions.getApi();
1218
+ if (command.kind === "settings" || command.kind === "token") {
1219
+ await actions.status();
1220
+ let payload;
1221
+ if (command.action !== "read") {
1222
+ securePayload = await readProtectedInput(dependencies.stdin ?? process.stdin, signal, "Settings JSON");
1223
+ try {
1224
+ payload = JSON.parse(securePayload);
1225
+ } catch {
1226
+ throw new InvocationError("Protected stdin must contain one JSON object.");
1227
+ }
1228
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new InvocationError("Protected stdin must contain one JSON object.");
1229
+ }
1230
+ const settings = command.kind === "settings" ? command.action === "read" ? { outcome: "read", settings: await actionApi.getSettings() } : await actionApi.saveSettings(payload) : void 0;
1231
+ const tokenMetadata = command.kind === "token" && command.action === "read" ? await actionApi.getTokenMetadata() : void 0;
1232
+ const token = command.kind === "token" && command.action === "replace" ? await actionApi.replaceToken(payload) : void 0;
1233
+ emit({
1234
+ version: 1,
1235
+ command: command.kind + " " + command.action,
1236
+ outcome: "success",
1237
+ reason: settings?.outcome ?? token?.outcome,
1238
+ ...settings ? { settings: settings.settings } : {},
1239
+ ...token ? { token } : {},
1240
+ ...tokenMetadata ? { tokenMetadata } : {},
1241
+ target: { environment: target.environment, origin: target.origin },
1242
+ profile,
1243
+ elapsedMs: Math.round(performance.now() - started),
1244
+ metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
1245
+ }, streams);
1246
+ return 0;
1247
+ }
1248
+ if (command.kind === "candle-export") {
1249
+ await actions.status();
1250
+ const candleExport = await actionApi.exportCandles({
1251
+ exchange: command.exchange,
1252
+ tradingSymbol: command.tradingSymbol,
1253
+ ...command.date ? { date: command.date } : {}
1254
+ });
1255
+ const download = command.output !== void 0 ? await downloadCandleExport(candleExport.url, command.output, signal, dependencies.fetchImpl) : void 0;
1256
+ emit({
1257
+ version: 1,
1258
+ command: command.kind,
1259
+ outcome: "success",
1260
+ ...download ? { download } : {},
1261
+ target: { environment: target.environment, origin: target.origin },
1262
+ profile,
1263
+ observedOperator: target.operatorId,
1264
+ candleExport,
1265
+ elapsedMs: Math.round(performance.now() - started),
1266
+ metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
1267
+ }, streams);
1268
+ return 0;
1269
+ }
1270
+ if (command.kind === "pair") {
1271
+ await actions.status();
1272
+ const result2 = await runPairCommand(command, actionApi);
1273
+ emit({
1274
+ version: 1,
1275
+ command: `pair ${command.action}`,
1276
+ outcome: "success",
1277
+ ...result2,
1278
+ target: { environment: target.environment, origin: target.origin },
1279
+ profile,
1280
+ observedOperator: target.operatorId,
1281
+ elapsedMs: Math.round(performance.now() - started),
1282
+ metadata: { requestId: actionApi.responseMetadata().requestId ?? null, buildId: actionApi.responseMetadata().buildId ?? null }
1283
+ }, streams);
1284
+ return 0;
1285
+ }
1286
+ if (command.kind === "zerodha") {
1287
+ await actions.status();
1288
+ if (command.otpStdin) otp = await readProtectedOtp(dependencies.stdin ?? process.stdin, signal);
1289
+ const zerodha = await runZerodhaAction(
1290
+ actionApi,
1291
+ command.action,
1292
+ target.operatorId,
1293
+ signal,
1294
+ command.attemptId,
1295
+ otp
1296
+ );
1297
+ const awaitingOtp = zerodha.status === "awaiting_otp";
1298
+ emit({
1299
+ version: 1,
1300
+ command: `zerodha ${command.action}`,
1301
+ outcome: awaitingOtp ? "blocked" : "success",
1302
+ reason: awaitingOtp ? "awaiting_otp" : "zerodha_session_present",
1303
+ target: { environment: target.environment, origin: target.origin },
1304
+ profile,
1305
+ observedOperator: target.operatorId,
1306
+ elapsedMs: Math.round(performance.now() - started),
1307
+ metadata: {
1308
+ requestId: actionApi.responseMetadata().requestId ?? null,
1309
+ buildId: actionApi.responseMetadata().buildId ?? null
1310
+ },
1311
+ zerodha
1312
+ }, streams);
1313
+ return awaitingOtp ? 4 : 0;
1314
+ }
1315
+ if (command.kind === "logout") {
1316
+ await actions.logout();
1317
+ emit({
1318
+ version: 1,
1319
+ command: "logout",
1320
+ outcome: "success",
1321
+ reason: "operator_session_cleared",
1322
+ target: { environment: target.environment, origin: target.origin },
1323
+ profile,
1324
+ elapsedMs: Math.round(performance.now() - started),
1325
+ metadata: {
1326
+ requestId: actionApi.responseMetadata().requestId ?? null,
1327
+ buildId: actionApi.responseMetadata().buildId ?? null
1328
+ }
1329
+ }, streams);
1330
+ return 0;
1331
+ }
1332
+ const result = await actions.loginFromSms();
1333
+ emit({
1334
+ version: 1,
1335
+ command: command.kind,
1336
+ outcome: "success",
1337
+ reason: "operator_session_established",
1338
+ target: { environment: target.environment, origin: target.origin },
1339
+ profile,
1340
+ observedOperator: result.observedOperator,
1341
+ elapsedMs: Math.round(performance.now() - started),
1342
+ metadata: { requestId: result.metadata?.requestId ?? null, buildId: result.metadata?.buildId ?? null }
1343
+ }, streams);
1344
+ return 0;
1345
+ } catch (error) {
1346
+ if (error instanceof OperatorSessionRejectedError && store && actionApi && target && owner !== void 0 && !signal?.aborted) {
1347
+ await store.saveSession(owner, target, actionApi.getCookies()).catch(() => void 0);
1348
+ }
1349
+ const failure = error instanceof SettingsActionError || error instanceof PriceCrossActionError ? { code: error.code, message: error.message, exitCode: 1 } : errorDetails(error, signal);
1350
+ const details = owner !== void 0 && profileNameOf(owner) === void 0 ? controllerSessionDetails(failure) : failure;
1351
+ const recovery = error instanceof Error ? error : void 0;
1352
+ const attemptId = recovery?.attemptId ?? (command?.kind === "zerodha" ? command.attemptId : void 0);
1353
+ emit({
1354
+ version: 1,
1355
+ command: command?.kind === "zerodha" || command?.kind === "settings" || command?.kind === "token" || command?.kind === "pair" ? `${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 ?? "",
1356
+ outcome: (error instanceof SettingsActionError || error instanceof PriceCrossActionError) && error.uncertain ? "uncertain" : details.exitCode === 4 ? "blocked" : "fail",
1357
+ reason: details.code,
1358
+ ...target ? { target: { environment: target.environment, origin: target.origin } } : command ? { target: { environment: command.environment } } : {},
1359
+ ...profile ? { profile } : {},
1360
+ ...attemptId ? { attemptId, recovery: recovery?.recovery ?? "Observe this exact attempt with the same target and profile; start explicitly only if Gate reports it inactive." } : {},
1361
+ ...!attemptId && error instanceof PriceCrossActionError && error.recovery ? { recovery: error.recovery } : {},
1362
+ ...recovery?.observedState ? { observedState: recovery.observedState } : {},
1363
+ ...recovery?.observedOperator ? { observedOperator: recovery.observedOperator } : {},
1364
+ ...details.nextStep ? { nextStep: details.nextStep } : {},
1365
+ elapsedMs: Math.round(performance.now() - started),
1366
+ metadata: {
1367
+ requestId: actionApi?.responseMetadata().requestId ?? null,
1368
+ buildId: actionApi?.responseMetadata().buildId ?? null
1369
+ },
1370
+ error: { code: details.code, message: details.message }
1371
+ }, streams, details.exitCode === 2 ? `Invalid CLI invocation: ${details.message}` : details.message);
1372
+ return details.exitCode;
1373
+ } finally {
1374
+ otp = void 0;
1375
+ securePayload = void 0;
1376
+ if (deadlineTimer) clearTimeout(deadlineTimer);
1377
+ }
1378
+ }
1379
+ function hasArgumentOtp(argv) {
1380
+ return argv.some((token) => token === "--otp" || token.startsWith("--otp="));
1381
+ }
1382
+
1383
+ export {
1384
+ downloadCandleExport,
1385
+ createOperatorSessionActions,
1386
+ runPairCommand,
1387
+ runOperatorSessionValidation,
1388
+ runZerodhaAction,
1389
+ runZerodhaValidation,
1390
+ parseCandleExportInstrument,
1391
+ isCandleExportDate,
1392
+ runHeadlessCli,
1393
+ hasArgumentOtp
1394
+ };